
Most software projects don’t fail because the problem was technically too hard. They fail because they become a tangled mess. You start with a clean, simple idea, write some code, and it works. Then you add a feature. And another. Soon you’re afraid to touch anything because changing one part breaks three others you didn’t know were connected. The project collapses under its own accumulated complexity.
The solution, in software, is almost always to break large, complex things into small, simple things. We do this with functions. You don’t write a thousand lines of code in a single block; you group related lines into a function with a name. This is so obvious we don’t even think about it. A component is the same idea, but for the user interface.
Think of a web page not as a single document, but as a collection of Lego bricks. Each brick is a component. It’s a self-contained piece of the interface. It has its own structure, its own styles, and its own behavior. You might have a UserProfile component. That component might itself be built from smaller components: an Avatar, a UserName, and a PostList. The PostList is in turn made of many Post components.
The power of this approach comes from encapsulation. The Avatar component doesn’t need to know anything about the PostList. Its only job is to display a picture. You can work on it in isolation, test it in isolation, and reuse it anywhere you need an avatar. This dramatically reduces cognitive load. You’re no longer trying to hold the entire application in your head, just the small part you’re currently working on.
Before components, you might have a large HTML file that looked something like this:
<div class="main-content">
<h1>My Awesome App</h1>
<!-- User Profile Section -->
<div class="user-card">
<img src="/img/user123.png" alt="User Avatar">
<h2>Alice</h2>
<p>Building things for the web.</p>
</div>
<!-- Other sections... -->
</div>
With a component-based framework like Vue, your main structure becomes much cleaner. You’re composing with abstractions, not just tags.
<div class="main-content"> <h1>My Awesome App</h1> <UserProfile userId="123" /> <!-- Other sections... --> </div>
All the messy details of what a user profile looks like are hidden away inside the UserProfile component. You’ve created a new piece of vocabulary for your application. This is more than just a convenience. It’s a fundamental shift in how you build user interfaces. You stop thinking in terms of pages and start thinking in terms of a tree of self-contained units that communicate with one another. Getting this mental model right is more important than memorizing any specific API. The syntax is the easy part. The hard part is learning to see the UI not as a monolith, but as a hierarchy of discrete parts. Once you see it, you can’t unsee it. You’ll wonder how you ever built anything complex without it. The component is the primitive unit of modern web development. It’s the function, for the visual world.
Anker USB C Hub, 5-in-1 USBC to HDMI Splitter with 4K Display, 1 x Powered USB-C 5Gbps & 2×Powered USB-A 3.0 5Gbps Data Ports for MacBook Pro, MacBook Air, Dell and More
$16.71 (as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)The three parts of a component
A component is a bundle of three things: its structure, its logic, and its appearance. In Vue, these are typically housed together in a single .vue file. This might seem wrong at first. For years, we were taught to separate our concerns: HTML in one file, JavaScript in another, and CSS in a third. But that was a separation of *technologies*, not of *concerns*. The real concern is the component itself. A button’s style is more closely related to that button’s logic than it is to the style of a navigation bar on the other side of the page. Putting all the pieces of a component in one place is a more meaningful way to organize a project.
The code block contains the HTML. This is the skeleton of your component. It’s not static, though. It’s a living document that Vue controls. You can insert data directly into the HTML using double curly braces, like {{ count }}. This creates a binding. When the value of count changes in your script, the HTML updates automatically. You don’t have to write code to find the element and change its text content. Vue handles it. This is the core of what a reactive framework does: it connects your data to the DOM.
The block contains the JavaScript. This is the component’s brain. It’s where you define the data that the template will display and the functions that will modify that data. In modern Vue, you use a setup attribute on the script tag. Any variable or function you declare there is automatically available to be used in your template. There’s no complex ceremony for exporting and importing things. You define a piece of state, and you can immediately use it in the HTML.
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
Here, ref(0) creates a reactive piece of state. It’s an object that holds a value. To change it, you modify its .value property. This is how Vue knows that something has changed and that it might need to re-render the component.
Finally, the block holds the CSS. The most important thing here is the scoped attribute. When you write , Vue processes your CSS so that it only applies to the elements within the current component’s template. This solves one of the oldest and most frustrating problems in web development: CSS rules leaking out and unintentionally affecting other parts of the application. It’s another form of encapsulation. You can write simple, direct selectors like button { ... } and be confident that you’re only styling the button in *this* component, not every button on the entire site.
Putting it all together, a simple counter component looks like this:
<template>
<div class="counter-widget">
<p>Current count: {{ count }}</p>
<button @click="increment">Click me</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<style scoped>
.counter-widget {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
display: inline-block;
}
button {
background-color: #42b983;
color: white;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
}
</style>
This single file contains everything needed for this piece of UI to function. It has its structure, its behavior, and its appearance. It doesn’t depend on any external CSS or JavaScript to do its job. You can drop this file into a project and use in another component’s template. It just works. This is the essence of component-based design. You are creating self-sufficient building blocks. The template describes what to show, using data from the script. The script defines that data and the logic to change it. The style makes it look right, without interfering with anything else. These three parts work together to form a single, cohesive unit.
How parents talk to children
Components in a tree need to communicate. A parent component often needs to pass data down to its children. If you have a UserProfile component, the parent that uses it needs to tell it *which* user to display. This channel of communication from parent to child is called “props”.
Props are to components what arguments are to functions. When you call a function, you pass it arguments. When you use a component, you pass it props. They are custom attributes you can register on a component. The parent provides the values, and the child receives them.
Let’s say we have a Greeting component that needs to display a name. Inside the child component’s file, Greeting.vue, you declare the props it accepts using a special defineProps function.
<template>
<h1>Hello, {{ name }}!</h1>
</template>
<script setup>
defineProps({
name: String
})
</script>
Here, we’ve told Vue that this component expects to receive a prop called name, and that its value should be a string. Now, in a parent component, we can use Greeting and pass it a name, just like setting an attribute on an HTML tag.
<template> <Greeting name="Alice" /> <Greeting name="Bob" /> </template> <script setup> import Greeting from './Greeting.vue' </script>
This will render two greetings, one for Alice and one for Bob. We’ve made a reusable component and configured it from the outside. This is the most basic form of parent-child communication.
Passing a static string is useful, but the real power comes from binding props to dynamic data in the parent. If the parent’s data changes, the prop passed to the child will update, and the child will automatically re-render. You use the v-bind directive, or its shorthand :, to do this.
Imagine the parent component has a variable currentUserName that can change over time. You would bind it like this:
<template>
<div>
<input v-model="currentUserName" />
<Greeting :name="currentUserName" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Greeting from './Greeting.vue'
const currentUserName = ref('World')
</script>
Now, as you type in the input field, the currentUserName value in the parent changes. Vue detects this and efficiently sends the new value down to the Greeting component’s name prop. The <h1> inside Greeting updates instantly. You didn’t have to write any code to manually pass the new value or trigger an update. The system is reactive from top to bottom.
There is a critical rule here: the data flows one way. Downwards. A child component should never try to change a prop it received. Think of props as immutable from the child’s perspective. This is a deliberate constraint. If a child could change its parent’s state whenever it wanted, it would be impossible to reason about where data changes come from. Your application would quickly devolve into the same tangled mess components were meant to solve. The child can read the name prop, but it cannot do props.name = 'Charlie'. Vue will warn you if you try. This one-way data flow makes the state of your application predictable. To affect the parent’s state, the child must send a message up, asking the parent to make a change. That is a different communication channel. For now, what matters is that data comes down through props, and props are read-only for the receiver.
To make your components more robust, you can define your props with more detail than just the type. You can make them required, provide default values, or even write custom validation functions. This forms a clear contract between the parent and the child.
defineProps({
name: {
type: String,
required: true
},
age: {
type: Number,
default: 21
},
status: {
type: String,
validator: (value) => {
return ['active', 'inactive', 'pending'].includes(value)
}
}
})
This tells any developer using your component exactly what it needs to function correctly. If they forget to pass a required name, or pass an invalid status, Vue will warn them during development. This is how you build reliable, composable systems. You define clear boundaries and contracts between the parts. Props are the mechanism for defining the input contract for a component. They are the public API of your UI building blocks.
How children talk back
The one-way data flow of props is a deliberate and useful constraint. It makes applications predictable. But it leaves an obvious question: what if the child *needs* to tell the parent something? A generic Button component needs to tell its parent that it has been clicked. A custom FancyInput component needs to tell its parent that the user has typed something new. The child can’t change the parent’s state directly, so it needs a way to send a message up.
This upward communication channel is a system of events. It’s the inverse of props. Props flow down; events bubble up. A child component can emit a named event, and the parent can choose to listen for it. This is no different from how you listen for a native DOM event like click. You’re just creating your own custom event types.
To do this, the child component first needs to declare the events it might emit. This is done with the defineEmits macro, which is the counterpart to defineProps.
<!-- ChildComponent.vue -->
<script setup>
const emit = defineEmits(['notifyParent'])
function doSomethingAndNotify() {
// ... do some work ...
emit('notifyParent')
}
</script>
Here, we’ve declared that this component can emit an event called notifyParent. The defineEmits function returns an emit function, which we can then call to actually fire the event. In the parent, you listen for this event using the v-on directive, or the @ shorthand. Vue automatically converts the camelCase event name notifyParent to its kebab-case equivalent, notify-parent, for use in the template.
<!-- ParentComponent.vue -->
<template>
<ChildComponent @notify-parent="handleNotification" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
function handleNotification() {
console.log('The child sent a message!')
}
</script>
When doSomethingAndNotify is called inside the child, it emits the event. The parent, which is listening for @notify-parent, then executes its handleNotification method. The communication is explicit. The parent doesn’t know *why* the child emitted the event, only that it did. The child doesn’t know what the parent will *do* in response. This decoupling is a good thing. Each component has its own well-defined responsibilities.
Often, you need to send not just a signal, but also data. An event can carry a payload. When you call the emit function, you can pass additional arguments. These arguments will be passed along to the parent’s event handler function.
Let’s make a custom input component. It needs to tell the parent what the new value is.
<!-- CustomInput.vue -->
<template>
<input
type="text"
:value="modelValue"
@input="onInput"
/>
</template>
<script setup>
defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
function onInput(event) {
emit('update:modelValue', event.target.value)
}
</script>
This component accepts a modelValue prop to display in the input. When the user types, the native input event fires. Our onInput handler then emits a custom event, update:modelValue, and passes the input’s current value as the payload.
The parent can listen for this and use the payload to update its own state.
<!-- ParentComponent.vue -->
<template>
<p>Current message: {{ message }}</p>
<CustomInput
:model-value="message"
@update:model-value="newValue => message = newValue"
/>
</template>
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const message = ref('Hello Vue')
</script>
This pattern-a modelValue prop and an update:modelValue event-is so common that Vue provides a shortcut for it: v-model. Using v-model on a component is syntactic sugar for exactly what we just wrote by hand. The parent component can be simplified to this:
<template>
<p>Current message: {{ message }}</p>
<CustomInput v-model="message" />
</template>
This does the exact same thing. It binds the parent’s message data to the child’s modelValue prop, and it listens for the update:modelValue event to update message. This is how you create components that behave like native form elements. You are essentially teaching Vue a new two-way binding for your own custom building block. The mechanism is simple and explicit: a prop for data flowing down, and an event for messages flowing up. v-model just makes using that mechanism more convenient. It’s a convention built on top of the fundamental primitives of props and events. Understanding this convention is key to building complex forms and interactive components that feel natural to use for other developers. You can even have multiple v-model bindings on a single component by giving them arguments, like v-model:title and v-model:content. This would correspond to title and content props, and update:title and update:content events. This allows for a very expressive API when a component needs to manage multiple pieces of data for its parent. The core idea remains the same: the parent owns the state, and the child requests changes via events.
