Components and Props
Defining components, declaring props and emits, and two-way binding with v-model.
Defining and using a component
Every .vue file is itself a reusable component. To use one inside another, import it and reference it by tag name in the template — with <script setup>, an imported component is automatically available in the template with no extra registration step:
<!-- UserCard.vue -->
<script setup>
defineProps(["name", "role"]);
</script>
<template>
<div class="card">
<h3>{{ name }}</h3>
<p>{{ role }}</p>
</div>
</template>
<!-- App.vue -->
<script setup>
import UserCard from "./UserCard.vue";
</script>
<template>
<div>
<UserCard name="Ada Lovelace" role="Engineer" />
<UserCard name="Grace Hopper" role="Admiral" />
</div>
</template>
defineProps: declaring what a component accepts
defineProps is a compiler macro — available automatically inside <script setup> with no import needed — that declares the props a component accepts. The array form above is the simplest version; a real component should almost always use the object form instead, which lets you specify types, mark props required, and give defaults:
<script setup>
const props = defineProps({
name: { type: String, required: true },
role: { type: String, default: "Member" },
age: { type: Number, required: false },
});
</script>
<template>
<div class="card">
<h3>{{ name }}</h3>
<p>{{ role }}</p>
</div>
</template>
Vue validates these at runtime in development and warns in the console if a required prop is missing or the wrong type is passed — genuinely useful for catching integration bugs early. Like React props, Vue props flow one way, parent to child, and a component should never reassign a prop it received.
With TypeScript, the same thing is usually written with a type instead:
<script setup lang="ts">
interface Props {
name: string;
role?: string;
}
const props = withDefaults(defineProps<Props>(), {
role: "Member",
});
</script>
defineEmits: emitting events to the parent
Props flow down; events flow back up. A child component announces that something happened by emitting a named event, which the parent listens for with v-on/@, the same directive used for native DOM events:
<!-- LikeButton.vue -->
<script setup>
const emit = defineEmits(["liked"]);
function handleClick() {
emit("liked", { timestamp: Date.now() });
}
</script>
<template>
<button @click="handleClick">Like</button>
</template>
<!-- App.vue -->
<script setup>
import LikeButton from "./LikeButton.vue";
function onLiked(payload) {
console.log("Liked at", payload.timestamp);
}
</script>
<template>
<LikeButton @liked="onLiked" />
</template>
defineEmits declares which event names the component can emit (useful for documentation and validation, same spirit as defineProps), and the returned emit function actually fires one, optionally with a payload.
v-model on a custom component
v-model is the two-way binding directive Vue uses for form inputs (<input v-model="text" />). You can support it on your own components too — it's really shorthand for a modelValue prop paired with an update:modelValue event:
<!-- CustomInput.vue -->
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<!-- App.vue -->
<script setup>
import { ref } from "vue";
import CustomInput from "./CustomInput.vue";
const text = ref("");
</script>
<template>
<CustomInput v-model="text" />
<p>You typed: {{ text }}</p>
</template>
Writing <CustomInput v-model="text" /> expands to :modelValue="text" @update:modelValue="text = $event" — the child receives the current value as a prop and reports changes by emitting an event, and v-model is just the compiler-level sugar that wires those two directions together in one directive. A component can even support multiple named v-models (v-model:title, v-model:content) by following the same propName / update:propName pattern with a name other than the default modelValue.
Common mistakes
- Mutating a prop directly inside the child (
props.name = "x") — props are one-way; the parent owns that value. - Forgetting to declare an emitted event with
defineEmits— it still works at runtime, but you lose Vue's validation and the documentation value of an explicit list. - Wiring up
v-modelmanually with a differently-named prop/event pair without realizingmodelValue/update:modelValueis the default conventionv-modelexpects — leading to "why doesn't v-model work on my component" confusion. - Confusing props (parent → child data) with emitted events (child → parent notifications) — data flows down, and only through props; anything the parent needs to react to gets there via an emitted event, never by the child reaching back into the parent's state.