defineModel()

The defineModel() macro is used to create two-way data binding between parent and child components. It automatically creates a prop and an emit event, creating a clean and simple approach to passing data between parent and child components.

It is only available to use inside of <script setup>, and importing is not required. The defineModel() macro was introduced in Vue 3.4+ and provides a cleaner alternative to manually implementing two-way binding with defineProps() and defineEmits().

If not using <script setup>, you would need to manually implement two-way binding using props and emits options with modelValue and update:modelValue.

Syntax

// Basic syntax
const model = defineModel();

// With type definition
const model = defineModel({
  type: Type,
  required: Boolean,
  default: DefaultValue
});

// With custom name (instead of modelValue)
const title = defineModel('title', {
  type: String,
  required: true
});

Parameters:

  • First parameter (optional): A string representing the model name. If omitted, defaults to 'modelValue'.
  • Second parameter (optional): An object with the following properties:
    • type: The constructor function (String, Number, Boolean, Date, Array, Object, etc.).
    • required: If the model is required (Boolean).
    • default: The default value or a function that returns the default value.

Return Value:

Returns a ref() that can be used directly with v-model in the template. Changes to this ref automatically sync with the parent component.

Basic usage

Here's a simple example of using defineModel() in an input component. This component creates two-way binding for a text input:

<script setup>
const model = defineModel({
  type: String,
  default: ''
});
</script>

<template>
  <input v-model="model" type="text" placeholder="Enter text" />
</template>

The component is used in the parent with v-model:

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

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

<template>
  <CustomInput v-model="textValue" />
  <p>Current value: {{ textValue }}</p>
</template>

Data is now automatically passed between parent and child components.

The defineModel() macro automatically creates a modelValue prop and an update:modelValue emit event. When you use v-model on the component, Vue handles the prop binding and event listening automatically.

Type validation

You can specify the type of data the model accepts:

// String model
const name = defineModel({
  type: String,
  default: ''
});

// Number model
const count = defineModel({
  type: Number,
  default: 0
});

// Boolean model
const isActive = defineModel({
  type: Boolean,
  default: false
});

// Array model
const items = defineModel({
  type: Array,
  default: () => []
});

// Object model
const user = defineModel({
  type: Object,
  default: () => ({})
});

Required models

You can mark a model as required to ensure it's always provided by the parent:

const title = defineModel({
  type: String,
  required: true
});

If the parent doesn't provide a value for a required model, Vue will show a warning in the console.

Default values

Models can have default values when not provided by the parent:

// Simple default value
const message = defineModel({
  type: String,
  default: 'Hello World'
});

// Function that returns default (for objects and arrays)
const formData = defineModel({
  type: Object,
  default: () => ({
    name: '',
    email: '',
    age: 0
  })
});

const tags = defineModel({
  type: Array,
  default: () => []
});

Custom model names

By default, defineModel() uses modelValue as the prop name. You can specify a custom name as the first parameter:

<script setup>
const title = defineModel('title', {
  type: String,
  required: true
});

const description = defineModel('description', {
  type: String,
  default: ''
});
</script>

<template>
  <input v-model="title" type="text" placeholder="Title" />
  <textarea v-model="description" placeholder="Description"></textarea>
</template>

In the parent component, use the custom name with v-model:

<script setup>
import { ref } from 'vue';
import CustomForm from './CustomForm.vue';

const formTitle = ref('');
const formDescription = ref('');
</script>

<template>
  <CustomForm
    v-model:title="formTitle"
    v-model:description="formDescription"
  />
</template>

Multiple models

You can use multiple defineModel() calls to create multiple two-way bindings:

<script setup>
const firstName = defineModel('firstName', {
  type: String,
  default: ''
});

const lastName = defineModel('lastName', {
  type: String,
  default: ''
});

const email = defineModel('email', {
  type: String,
  default: ''
});
</script>

<template>
  <div class="form">
    <input v-model="firstName" type="text" placeholder="First Name" />
    <input v-model="lastName" type="text" placeholder="Last Name" />
    <input v-model="email" type="email" placeholder="Email" />
  </div>
</template>

Parent usage:

<template>
  <UserForm
    v-model:first-name="user.firstName"
    v-model:last-name="user.lastName"
    v-model:email="user.email"
  />
</template>

Practical example: Form component

The following example shows how defineModel() can be used in a form component to create two-way binding for form data:

<script setup>
const formData = defineModel({
  type: Object,
  default: () => ({
    name: '',
    email: '',
    message: ''
  })
});
</script>

<template>
  <form class="contact-form">
    <div class="form-group">
      <label for="name">Name</label>
      <input
        id="name"
        v-model="formData.name"
        type="text"
        placeholder="Your name"
      />
    </div>
    <div class="form-group">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="formData.email"
        type="email"
        placeholder="[email protected]"
      />
    </div>
    <div class="form-group">
      <label for="message">Message</label>
      <textarea
        id="message"
        v-model="formData.message"
        placeholder="Your message"
        rows="4"
      ></textarea>
    </div>
  </form>
</template>

The parent component uses v-model to bind the form data:

<script setup>
import { ref } from 'vue';
import ContactForm from './ContactForm.vue';

const contactFormData = ref({
  name: '',
  email: '',
  message: ''
});
</script>

<template>
  <ContactForm v-model="contactFormData" />
  <div class="preview">
    <h3>Preview:</h3>
    <p>Name: {{ contactFormData.name }}</p>
    <p>Email: {{ contactFormData.email }}</p>
    <p>Message: {{ contactFormData.message }}</p>
  </div>
</template>

Comparison: Traditional vs defineModel

Traditional approach (before Vue 3.4)

<script setup>
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
});

const emit = defineEmits(['update:modelValue']);
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
    type="text"
  />
</template>

Modern approach with defineModel

<script setup>
const model = defineModel({
  type: String,
  default: ''
});
</script>

<template>
  <input v-model="model" type="text" />
</template>

The defineModel() approach is much simpler and requires less boilerplate code!

Using with TypeScript

When using TypeScript, you can define models with explicit types:

// String model
const title = defineModel<string>({
  default: ''
});

// Number model
const count = defineModel<number>({
  default: 0
});

// Object model with interface
interface User {
  name: string;
  email: string;
  age: number;
}

const user = defineModel<User>({
  default: () => ({
    name: '',
    email: '',
    age: 0
  })
});