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:

HTML
<!-- UserCard.vue -->
<script setup>
defineProps(["name", "role"]);
</script>

<template>
  <div class="card">
    <h3>{{ name }}</h3>
    <p>{{ role }}</p>
  </div>
</template>
HTML
<!-- 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:

HTML
<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:

HTML
<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:

HTML
<!-- LikeButton.vue -->
<script setup>
const emit = defineEmits(["liked"]);

function handleClick() {
  emit("liked", { timestamp: Date.now() });
}
</script>

<template>
  <button @click="handleClick">Like</button>
</template>
HTML
<!-- 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:

HTML
<!-- CustomInput.vue -->
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>
HTML
<!-- 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-model manually with a differently-named prop/event pair without realizing modelValue/update:modelValue is the default convention v-model expects — 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.