| |

Vue.js 30 🟢 Emitting Custom Events with defineEmits() and Event Validation

Props flow down from parent to child. Events flow up from child to parent. When a child component needs to tell its parent that something happened — a button was clicked, a value changed, a form was submitted — it emits an event. The parent listens for that event and reacts. This is the second half of Vue’s component communication model, and defineEmits() is the macro that declares which events a component can emit.

Like defineProps(), defineEmits() is a compiler macro. It is used inside <script setup>, and the compiler reads the declaration and generates the runtime emits option. The declaration serves two purposes. First, it documents the component’s event interface: which events the component emits and what payload each event carries. Second, it enables Vue’s runtime validation in development mode. When a component emits an event that is not declared, or emits a payload that does not match the declared type, Vue logs a warning in the console.

The macro supports two declaration styles, parallel to defineProps(). Runtime declaration uses an array of event names or an object with validator functions. Type-based declaration uses a TypeScript generic with call signatures, where each signature describes the event name and its parameters. The two styles are mutually exclusive. The type-based style provides better IDE support and compile-time checking, while the runtime style provides explicit validation functions.

This chapter covers three areas. First, why defineEmits() exists — the problem of declaring a component’s event interface and the difference between props and events. Second, how each declaration style works — the runtime array and object syntax, and the type-based call signature syntax. Third, how to emit events and validate them — the emit function, payload validation, the v-model integration with update:modelValue, and the patterns for passing data upward. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the event flow.

Key point: defineEmits() is a compiler macro that declares a component’s custom events. It supports runtime declaration (array or object with validators) and type-based declaration (TypeScript call signatures). The returned emit function sends events to the parent. Events are validated in development mode, and undeclared events produce a warning.


Why defineEmits() exists

The upward-communication problem. Props flow in one direction: parent to child. A child cannot modify a prop, and it cannot reach up to change the parent’s state directly. But children often need to request changes. A form component needs to tell the parent that the user submitted the form. A dropdown needs to tell the parent which option was selected. A modal needs to tell the parent that the user closed it. Events are the mechanism for this upward communication. The child emits an event, and the parent listens for it and handles the change .

The interface-declaration problem. Without a declaration, the set of events a component emits is implicit in the emit() calls scattered throughout the component. The parent has no way to know which events to listen for, and the compiler has no way to validate that the emitted event names and payloads are correct. defineEmits() makes the event interface explicit. The declaration is the contract, and Vue validates it at runtime in development mode .

The payload-validation problem. An event can carry a payload — the data the child wants to send to the parent. Without validation, the payload can be anything, and the parent has no guarantee about its shape. The runtime declaration supports a validator function that receives the payload and returns true or false. If the validator returns false, Vue logs a warning. The type-based declaration validates the payload at compile time, because the TypeScript compiler checks the arguments to emit() against the declared signatures .

The v-model problem. Vue’s v-model directive is syntactic sugar for a prop and an event. On a component, v-model="value" expands to :modelValue="value" and @update:modelValue="value = $event". The child declares a modelValue prop and emits an update:modelValue event. This pattern is the standard way to build two-way-bound components, and defineEmits() is the macro that declares the update:modelValue event. Named v-model arguments like v-model:title use the same pattern with update:title .

The Options-API migration problem. In the Options API, events are declared in the emits option of the component object. In <script setup>, there is no component object. defineEmits() fills that role. It is the <script setup> equivalent of the emits option, with the same semantics and a more concise syntax.

The trade-off. The declaration adds a small amount of ceremony. A component that emits a single event still needs to declare it, and the declaration is separate from the emit() call. But the declaration is the interface, and the interface is what makes the component usable. Without it, the parent must read the component’s source to discover which events to listen for. With it, the events are documented, validated, and type-checked.


a. Runtime declaration — array and object syntax

The simplest declaration is an array of event names. This tells Vue which events the component can emit, and it enables validation that no undeclared events are emitted .

<script setup>
const emit = defineEmits(['change', 'submit', 'close']);
</script>

The array form does not validate payloads. It only validates event names. An event emitted with any payload is accepted, as long as the event name is in the array.

The object form adds validator functions. Each key is an event name, and the value is a function that receives the payload and returns true or false .

<script setup>
const emit = defineEmits({
  change: (payload) => {
    return typeof payload === 'string' && payload.length > 0;
  },
  submit: (payload) => {
    return payload && typeof payload.email === 'string';
  },
  close: null, // no validation
});
</script>

A null value means the event is declared but has no validator. The validator is called when the event is emitted. If it returns false, Vue logs a warning in the development console. The event is still emitted; the validator does not block it. The warning is a debugging aid, not a runtime constraint .

The runtime declaration is the right choice for JavaScript projects, or for cases where the payload validation logic is complex and cannot be expressed in TypeScript types.


b. Type-based declaration — call signatures

The type-based declaration uses a TypeScript generic with call signatures. Each signature describes an event name and its parameters. The syntax uses named tuple elements to give the parameters names, which improves the IDE experience .

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void;
  (e: 'update', value: string): void;
  (e: 'delete', id: number, reason?: string): void;
}>();
</script>

Each call signature defines one event. The first parameter is the event name, prefixed with e: for documentation. The remaining parameters are the event’s payload. The return type is void. When the component calls emit('change', 1), the compiler checks that 'change' is a declared event and that the second argument is a number. If the argument type does not match, the code does not compile .

A more concise syntax uses named tuple elements, which is supported in Vue 3.3 and later:

<script setup lang="ts">
const emit = defineEmits<{
  change: [id: number];
  update: [value: string];
  delete: [id: number, reason?: string];
}>();
</script>

The tuple syntax is shorter and provides the same type checking. The event name is the key, and the value is a tuple of the event’s parameters. The parameter names in the tuple are optional but improve readability .

The type-based declaration can extract the signatures to a type alias:

<script setup lang="ts">
type Emits = {
  change: [id: number];
  update: [value: string];
};

const emit = defineEmits<Emits>();
</script>

For components that need to emit events and also pass them through to a parent, the defineEmits type can be referenced with InstanceType:

<script setup lang="ts">
const emit = defineEmits<{
  change: [id: number];
}>();

// Pass the emit function to a composable
function useSomething(emit: (event: 'change', id: number) => void) {
  // ...
}
</script>

c. Emitting events and the v-model integration

The return value of defineEmits() is an emit function. It takes the event name as the first argument and the payload as the remaining arguments .

<script setup>
const emit = defineEmits(['increment', 'decrement']);

function increment() {
  emit('increment', 1);
}

function decrement() {
  emit('decrement', 1);
}
</script>

The parent listens for the event with @event-name or v-on:event-name:

<template>
  <Counter @increment="handleIncrement" @decrement="handleDecrement" />
</template>

The $event variable in the parent’s handler contains the payload. If the event has multiple arguments, $event contains the first one, and the handler must use an inline arrow function to access the rest:

<template>
  <!-- Single payload: $event is the id -->
  <Child @change="handleChange($event)" />

  <!-- Multiple payloads: use an arrow function -->
  <Child @delete="(id, reason) => handleDelete(id, reason)" />
</template>

The v-model integration is the most important pattern. When a parent uses v-model="value" on a component, Vue expands it to:

<Child
  :modelValue="value"
  @update:modelValue="value = $event"
/>

The child declares a modelValue prop and emits update:modelValue:

<script setup>
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();

function updateValue(newValue: string) {
  emit('update:modelValue', newValue);
}
</script>

<template>
  <input
    :value="modelValue"
    @input="updateValue($event.target.value)"
  />
</template>

Named v-model arguments use the same pattern with a different name. v-model:title="title" expands to :title="title" and @update:title="title = $event". The child declares a title prop and emits update:title .

A component can support multiple v-model bindings by declaring multiple props and their corresponding update: events:

<script setup lang="ts">
defineProps<{
  firstName: string;
  lastName: string;
}>();

defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
}>();
</script>

The parent uses v-model:firstName and v-model:lastName to bind both:

<template>
  <NameInput
    v-model:firstName="first"
    v-model:lastName="last"
  />
</template>

Complete Example Session

<!-- ============================================
     PART 1: RUNTIME ARRAY DECLARATION
     ============================================ -->

<script setup>
const emit = defineEmits(['change', 'submit', 'close']);

function handleChange(value) {
  emit('change', value);
}
</script>

<template>
  <input @input="handleChange($event.target.value)" />
  <button @click="emit('submit')">Submit</button>
  <button @click="emit('close')">Close</button>
</template>


<!-- ============================================
     PART 2: RUNTIME OBJECT DECLARATION WITH VALIDATORS
     ============================================ -->

<script setup>
const emit = defineEmits({
  change: (value) => {
    if (typeof value !== 'string') {
      console.warn('change event expects a string');
      return false;
    }
    return true;
  },
  submit: (payload) => {
    return payload && typeof payload.email === 'string';
  },
  close: null, // no validation
});
</script>

<template>
  <button @click="emit('submit', { email: 'a@b.com' })">Submit</button>
</template>


<!-- ============================================
     PART 3: TYPE-BASED CALL SIGNATURES
     ============================================ -->

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'change', id: number): void;
  (e: 'update', value: string): void;
  (e: 'delete', id: number, reason?: string): void;
}>();
</script>

<template>
  <button @click="emit('change', 1)">Change</button>
  <button @click="emit('delete', 1, 'duplicate')">Delete</button>
</template>


<!-- ============================================
     PART 4: TYPE-BASED TUPLE SYNTAX
     ============================================ -->

<script setup lang="ts">
const emit = defineEmits<{
  change: [id: number];
  update: [value: string];
  delete: [id: number, reason?: string];
}>();
</script>

<template>
  <button @click="emit('change', 1)">Change</button>
</template>


<!-- ============================================
     PART 5: EMITTING WITH PAYLOAD
     ============================================ -->

<script setup>
const emit = defineEmits(['select']);

function selectItem(item) {
  emit('select', item);
}
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id" @click="selectItem(item)">
      {{ item.name }}
    </li>
  </ul>
</template>


<!-- ============================================
     PART 6: THE PARENT LISTENING
     ============================================ -->

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

const selected = ref(null);

function handleSelect(item) {
  selected.value = item;
}
</script>

<template>
  <Child @select="handleSelect" />
  <p>Selected: {{ selected?.name }}</p>
</template>


<!-- ============================================
     PART 7: v-model INTEGRATION
     ============================================ -->

<!-- CustomInput.vue -->
<script setup lang="ts">
defineProps<{ modelValue: string }>();
const emit = defineEmits<{
  'update:modelValue': [value: string];
}>();

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement;
  emit('update:modelValue', target.value);
}
</script>

<template>
  <input :value="modelValue" @input="handleInput" />
</template>


<!-- ============================================
     PART 8: USING THE v-model COMPONENT
     ============================================ -->

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const name = ref('');
</script>

<template>
  <CustomInput v-model="name" />
  <p>Name: {{ name }}</p>
</template>

<!-- v-model="name" expands to:
     :modelValue="name"
     @update:modelValue="name = $event" -->


<!-- ============================================
     PART 9: MULTIPLE v-model BINDINGS
     ============================================ -->

<!-- NameInput.vue -->
<script setup lang="ts">
defineProps<{
  firstName: string;
  lastName: string;
}>();

defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
}>();
</script>

<template>
  <input
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue';
import NameInput from './NameInput.vue';

const first = ref('');
const last = ref('');
</script>

<template>
  <NameInput v-model:firstName="first" v-model:lastName="last" />
  <p>{{ first }} {{ last }}</p>
</template>


<!-- ============================================
     PART 10: THE COMPLETE COMPONENT
     ============================================ -->

<!-- SearchBox.vue -->
<script setup lang="ts">
import { ref } from 'vue';

const props = defineProps<{
  modelValue: string;
  placeholder?: string;
}>();

const emit = defineEmits<{
  'update:modelValue': [value: string];
  search: [query: string];
  clear: [];
}>();

const inputRef = ref<HTMLInputElement | null>(null);

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement;
  emit('update:modelValue', target.value);
}

function handleSearch() {
  emit('search', props.modelValue);
}

function handleClear() {
  emit('update:modelValue', '');
  emit('clear');
  inputRef.value?.focus();
}
</script>

<template>
  <div class="search-box">
    <input
      ref="inputRef"
      :value="modelValue"
      :placeholder="placeholder"
      @input="handleInput"
      @keyup.enter="handleSearch"
    />
    <button @click="handleSearch">Search</button>
    <button @click="handleClear">Clear</button>
  </div>
</template>

<!-- Parent.vue -->
<script setup>
import { ref } from 'vue';
import SearchBox from './SearchBox.vue';

const query = ref('');

function onSearch(q) {
  console.log('Searching for:', q);
}

function onClear() {
  console.log('Cleared');
}
</script>

<template>
  <SearchBox
    v-model="query"
    placeholder="Type to search..."
    @search="onSearch"
    @clear="onClear"
  />
</template>

The ten parts show the runtime array declaration, the runtime object declaration with validators, type-based call signatures, the tuple syntax, emitting with a payload, the parent listening, v-model integration, using the v-model component, multiple v-model bindings, and a complete search box component.


Quick Reference

Declaration Styles

StyleSyntaxTypeScriptValidation
Runtime arraydefineEmits(['change'])OptionalNames only
Runtime objectdefineEmits({ change: (v) => true })OptionalNames + payload
Type-based calldefineEmits<{ (e: 'change', id: number): void }>()RequiredCompile-time
Type-based tupledefineEmits<{ change: [id: number] }>()RequiredCompile-time

The emit Function

CallResult
emit('change')Emit event with no payload
emit('change', value)Emit event with one payload
emit('change', a, b)Emit event with multiple payloads

v-model Expansion

TemplateExpands to
v-model="value":modelValue="value" @update:modelValue="value = $event"
v-model:title="title":title="title" @update:title="title = $event"

Parent Listening

TemplateHandler receives
@change="handler"$event is the first payload
@change="(a, b) => handler(a, b)"All payloads
@update:modelValue="value = $event"The new value

Best Practices

✅ Do This:

<script setup lang="ts">
// Use type-based declaration
const emit = defineEmits<{
  change: [id: number];
  submit: [payload: FormData];
}>();                                                                     // ✅
// Use tuple syntax for conciseness
const emit = defineEmits<{ change: [id: number] }>();                    // ✅
// Use runtime validators for complex payloads
const emit = defineEmits({ change: (v) => typeof v === 'string' });      // ✅
// Use update:modelValue for v-model
defineEmits<{ 'update:modelValue': [value: string] }>();                 // ✅
// Name events in past tense or as actions
defineEmits<{ submitted: [data: Data] }>();                              // ✅
</script>

❌ Don’t Do This:

<script setup>
// Don't emit undeclared events
const emit = defineEmits(['change']);
emit('unknown'); // warning                                              // ❌
// Don't pass the wrong payload type
emit('change', 'string'); // if declared as number                       // ❌
// Don't mutate the payload object after emitting
emit('submit', form); form.reset(); // parent may still read it          // ❌
// Don't use v-model without the update: event
defineProps<{ modelValue: string }>(); // no emit declared               // ❌
// Don't mix runtime and type-based declaration
defineEmits(['change']); defineEmits<{ change: [id: number] }>();        // ❌
</script>

Common Pitfalls

PitfallWhy It HappensFix
Undeclared event warningEvent not in declarationAdd to defineEmits
Payload validator warningPayload does not match validatorFix the payload or the validator
v-model does not workMissing update:modelValue emitDeclare and emit the event
Parent handler not calledEvent name mismatchCheck the event name casing
Multiple payloads inaccessible$event is only the firstUse inline arrow function
Emit called before declarationemit used outside <script setup>Move logic inside setup
TypeScript error on emitWrong argument typeCheck the tuple signature

Real-World Examples

1. Runtime Array

const emit = defineEmits(['change', 'submit']);

2. Runtime Object with Validator

const emit = defineEmits({ change: (v) => typeof v === 'string' });

3. Type-Based Call Signature

const emit = defineEmits<{ (e: 'change', id: number): void }>();

4. Type-Based Tuple

const emit = defineEmits<{ change: [id: number] }>();

5. Emit with Payload

emit('select', item);

6. Parent Handler

<Child @select="handleSelect" />

7. v-model Component

const emit = defineEmits<{ 'update:modelValue': [value: string] }>();

8. Named v-model

defineEmits<{ 'update:title': [value: string] }>();

9. Multiple Payloads

emit('delete', id, reason);

10. Complete Search Box

const emit = defineEmits<{
  'update:modelValue': [value: string];
  search: [query: string];
  clear: [];
}>();

Visual

The Event Flow

┌──────────────────────────────────────────────────────────────┐
│  EVENT FLOW                                                  │
│                                                              │
│  Parent Component                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <Child @select="handleSelect" />                    │    │
│  │                                                       │    │
│  │  function handleSelect(item) {                       │    │
│  │    selected.value = item                             │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│    ▲                                                         │
│    │  emit('select', item)                                   │
│    │  Event flows up                                         │
│    │                                                         │
│  Child Component                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const emit = defineEmits(['select'])                │    │
│  │                                                       │    │
│  │  function selectItem(item) {                         │    │
│  │    emit('select', item)                              │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Props flow down. Events flow up.                            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Declaration Styles

┌──────────────────────────────────────────────────────────────┐
│  DECLARATION STYLES                                          │
│                                                              │
│  RUNTIME ARRAY:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  defineEmits(['change', 'submit', 'close'])          │    │
│  │                                                       │    │
│  │  Names only. No payload validation.                  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  RUNTIME OBJECT:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  defineEmits({                                       │    │
│  │    change: (v) => typeof v === 'string',             │    │
│  │    close: null                                       │    │
│  │  })                                                  │    │
│  │                                                       │    │
│  │  Names + payload validation.                         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  TYPE-BASED:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  defineEmits<{                                       │    │
│  │    change: [id: number]                              │    │
│  │    submit: [data: FormData]                          │    │
│  │  }>()                                                │    │
│  │                                                       │    │
│  │  Compile-time validation. IDE support.               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

v-model Expansion

┌──────────────────────────────────────────────────────────────┐
│  v-model EXPANSION                                           │
│                                                              │
│  Parent writes:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <CustomInput v-model="name" />                      │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  Vue expands to:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <CustomInput                                        │    │
│  │    :modelValue="name"                                │    │
│  │    @update:modelValue="name = $event"                │    │
│  │  />                                                  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Child declares:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  defineProps<{ modelValue: string }>()               │    │
│  │  defineEmits<{                                       │    │
│  │    'update:modelValue': [value: string]              │    │
│  │  }>()                                                │    │
│  │                                                       │    │
│  │  emit('update:modelValue', newValue)                 │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The prop carries the value down.                            │
│  The event carries the new value up.                         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Multiple v-model Bindings

┌──────────────────────────────────────────────────────────────┐
│  MULTIPLE v-model BINDINGS                                   │
│                                                              │
│  Parent:                                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <NameInput                                          │    │
│  │    v-model:firstName="first"                         │    │
│  │    v-model:lastName="last"                           │    │
│  │  />                                                  │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  Expands to:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  <NameInput                                          │    │
│  │    :firstName="first"                                │    │
│  │    @update:firstName="first = $event"                │    │
│  │    :lastName="last"                                  │    │
│  │    @update:lastName="last = $event"                  │    │
│  │  />                                                  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Child declares:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  defineProps<{ firstName: string; lastName: string }>()│   │
│  │  defineEmits<{                                       │    │
│  │    'update:firstName': [value: string]               │    │
│  │    'update:lastName': [value: string]                │    │
│  │  }>()                                                │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
defineEmits()Compiler macro for declaring custom events
Runtime arraydefineEmits(['change', 'submit'])
Runtime objectdefineEmits({ change: (v) => true })
Type-based calldefineEmits<{ (e: 'change', id: number): void }>()
Type-based tupledefineEmits<{ change: [id: number] }>()
emit functionemit('change', payload)
Payload validationRuntime validators or compile-time types
v-modelExpands to :modelValue + @update:modelValue
Named v-modelv-model:title → :title + @update:title
Development warningUndeclared events and failed validators

Key takeaways:

  • defineEmits() declares a component’s event interface. Like defineProps(), it is a compiler macro used inside <script setup>. The declaration documents which events the component emits and enables validation in development mode .
  • Runtime declaration supports array and object syntax. The array form validates event names only. The object form adds validator functions that receive the payload and return true or false. The validator is a debugging aid; it warns but does not block the event .
  • Type-based declaration uses call signatures or tuple syntax. The call signature form is { (e: 'change', id: number): void }. The tuple form is { change: [id: number] }. Both provide compile-time checking of event names and payload types .
  • The emit function sends events to the parent. It takes the event name as the first argument and the payload as the remaining arguments. The parent listens with @event-name and accesses the payload through $event or an inline arrow function .
  • v-model is syntactic sugar for a prop and an event. On a component, v-model="value" expands to :modelValue="value" and @update:modelValue="value = $event". The child declares the modelValue prop and emits the update:modelValue event .
  • Named v-model arguments follow the same pattern. v-model:title="title" expands to :title="title" and @update:title="title = $event". A component can support multiple v-model bindings by declaring multiple props and their corresponding update: events .
  • The declaration is the contract. Without it, the parent must read the component’s source to discover which events to listen for. With it, the events are documented, validated in development, and type-checked in TypeScript projects.
  • Payloads should be treated as immutable. Once an event is emitted, the parent may read the payload asynchronously. Mutating the payload after emitting can cause the parent to see the mutated value instead of the value at the time of the event.

Remember: Props flow down, events flow up. defineEmits() is the macro that declares the upward interface. The runtime declaration is explicit and works in JavaScript. The type-based declaration is concise and provides compile-time safety in TypeScript. The emit function sends the event, and the parent listens for it. v-model is the most important pattern, because it turns a prop-and-event pair into a single binding. The declaration is the contract between the component and its consumers, and the contract is what makes the component usable without reading its source.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!