How defineModel simplifies v-model in custom Vue components
Source: Dev.to
Introduction
In this article I demonstrate a practical use case for defineModel: a custom modal component that controls its visibility without needing a modelValue prop, an update:modelValue emit, or any boilerplate sync logic. defineModel replaces the traditional modelValue + update:modelValue pattern with a single reactive binding.
Using the Modal
import { ref } from 'vue';
import MyModal from './components/MyModal.vue';
const isModalOpen = ref(false);
Open Modern Modal
## Custom Content
This replaces the default slot content!
isModalOpenis just aref.v-modelcontrols the modal’s visibility.- The default slot lets the parent pass custom content, while the parent component remains unaware of the modal’s internal implementation.
Modal Implementation
import { useTemplateRef, watchEffect, defineModel } from 'vue';
// The magic: two‑way binding in one line
const isVisible = defineModel({ default: false });
const modal = useTemplateRef('modal');
watchEffect(() => {
if (!modal.value) return;
isVisible.value ? modal.value.showModal() : modal.value.close();
});
## Default Modal Title
This is the default content of your slot.
Close
What defineModel Does
const isVisible = defineModel({ default: false });
- Returns a reactive value.
- Provides automatic
v-modelsupport. - Enables two‑way binding without any explicit
emitcode.
Previously you would need:
const props = defineProps({ modelValue: Boolean });
const emit = defineEmits(['update:modelValue']);
// plus manual sync logic
Now it’s reduced to a single line.
Why a Modal Is a Perfect Fit
- It has a single source of truth: open / closed.
- The parent wants to control when it opens.
- The component itself needs internal control (close button, ESC key, etc.).
With defineModel, both sides can update the same value:
- Parent opens the modal →
isVisiblebecomestrue. - Modal closes itself →
isVisiblebecomesfalse.
The state stays in sync automatically—no custom events, no glue code.
Benefits of Using defineModel
- Cleaner API – the component behaves like a native input.
- Predictable behavior – only one source of truth.
- Minimal code – eliminates the “prop + emit + sync” ceremony.
- Reduced mental overhead – think of the component as having a single
valuerather than juggling multiple props and events.
Good Fit When
- The component has a single main state.
- Both parent and child need to update that state.
- You want a clean
v-modelAPI.
Example Scenarios
- Modal / dialog
- Toggle switch
- Tabs
- Drawer
- Select dropdown
- Stepper
Conclusion
defineModel is a small API change that dramatically simplifies the way we write custom Vue components. Instead of boilerplate, you get:
- One line to declare the bound value.
- One state that is shared automatically.
- One API (
v-model) for consumers.
If you haven’t tried defineModel yet, give it a shot in a real component. It makes v-model usage clearer and custom components easier to reason about.
Feel free to share your own use cases or suggestions in the comments.