Passing Data Downward with Props
Start by defining reactive state inside the parent component. Below, a ref named messageSource holds a string.
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue';
import ChildCard from './ChildCard.vue';
const messageSource = ref('Data originating from parent');
</script>
<template>
<ChildCard />
</template>
To expose this data to the child, bind a custom attribute on the child tag using v-bind or its shorthand :. The left side declares the prop name the child will receive; the right side references the parent's reactive value.
<!-- ParentComponent.vue -->
<template>
<ChildCard :transferredMessage="messageSource" />
</template>
Inside the child, define props via defineProps. The received value becomes available for interpolation inside the template or for use within <script setup> through the returned props object.
<!-- ChildCard.vue -->
<script setup>
const props = defineProps({
transferredMessage: String
});
</script>
<template>
<p>Child received: {{ props.transferredMessage }}</p>
</template>
Props can also include validation, default values, or multiple types. For instance:
defineProps({
transferredMessage: {
type: String,
default: 'Fallback text'
}
});
Emitting Events Upward
When a child needs to send enformation back, it declares custom events with defineEmits. The emitted event carries a payload that the parent can capture.
<!-- ChildSender.vue -->
<script setup>
const emit = defineEmits(['update-selection']);
function notifyParent() {
const payload = { id: 1, label: 'selected item' };
emit('update-selection', payload);
}
</script>
<template>
<button @click="notifyParent">Notify Parent</button>
</template>
The parent listens for the event using v-on or @, matching the event name exactly. The handler receives the emitted arguments.
<!-- ParentReceiver.vue -->
<script setup>
import { ref } from 'vue';
import ChildSender from './ChildSender.vue';
const currentSelection = ref(null);
function handleUpdate(data) {
currentSelection.value = data;
console.log('Payload from child:', data);
}
</script>
<template>
<ChildSender @update-selection="handleUpdate" />
<p>Held selection: {{ currentSelection }}</p>
</template>
Putting It Together
Props flow down; events flow up. In a shared view, both mechanisms often coexist:
<!-- Dashboard.vue -->
<script setup>
import { ref } from 'vue';
import StatusWidget from './StatusWidget.vue';
const systemStatus = ref('operational');
function processAlert(alertMsg) {
console.warn('Alert received:', alertMsg);
}
</script>
<template>
<StatusWidget :status="systemStatus" @trigger-alert="processAlert" />
</template>
<!-- StatusWidget.vue -->
<script setup>
const props = defineProps({
status: String
});
const emit = defineEmits(['trigger-alert']);
function raiseAlert() {
emit('trigger-alert', `Status changed to: ${props.status}`);
}
</script>
<template>
<div>
<span>Current status: {{ status }}</span>
<button @click="raiseAlert">Simulate Alert</button>
</div>
</template>
Both props and emits are compile-time macros that don't require explicit imports in <script setup>, though importing them is harmless and often used for explicitnes.