In Vue 3, what do v-model's .lazy, .number and .trim modifiers change on a text input, and why can v-model.number still store a string?
answer
- which DOM event triggers the sync
- parseFloat with a fallback
- empty field is not zero
- type="number" applies it for you
basics
~20 sOn a Vue 3 text input, .lazy syncs on change instead of input, .trim strips surrounding whitespace, and .number casts with parseFloat. When parseFloat fails, such as on an empty field, .number keeps the original string, so the model can hold ''.
solid answer
~40 sBy default `v-model` on a text input writes the model on every `input` event (except during IME composition). `.lazy` switches that to the `change` event, which a text field fires when the edit is committed, typically on blur or Enter. `.trim` trims the value before it is written. `.number` runs the value through `parseFloat`, and **if the result is `NaN` it keeps the original string** — so clearing the field stores `''`, not `0`, `null` or `NaN`. Vue applies `.number` automatically when the input has `type="number"`. On `change`, Vue also writes the trimmed or cast value back into the field. So a `v-model.number` age field is really typed `number | string`, and validation has to check for the empty string.
code
vue · 12 lines<script setup lang="ts">
import { ref } from 'vue'
const email = ref('')
const age = ref<number | string>('')
</script>
<template>
<input v-model.lazy.trim="email" type="email" />
<input v-model.number="age" inputmode="numeric" />
<p>age is {{ typeof age }}: {{ JSON.stringify(age) }}</p>
</template>go deeper
Recall what each modifier does: .lazy uses the change event, .trim strips whitespace, .number casts to a number.
Explain the parseFloat fallback, the empty-string case, the automatic cast for type="number", and how the modifiers combine.
Show the production consequence: type the model as number | string, validate the empty case, and pick .lazy for fields that trigger expensive watchers.
Decide where numeric normalisation belongs in a shared form layer — in the template modifier, a writable computed, or the submit mapper — and make it consistent.
## What v-model does on a text input without modifiers On an `<input>` with a text-like type or a `<textarea>`, Vue 3 expands `v-model` into a runtime directive (`vModelText`) that: - sets the element's `value` property from the model when the component mounts and updates; - listens for the `input` event and writes `el.value` into the model after every keystroke; - skips those writes while an **IME composition** is in progress, then syncs once when the composition ends. The modifiers change three separate steps of that pipeline. ## The three modifiers | Modifier | What it changes | Model after typing ` 42 ` and blurring | |---|---|---| | none | syncs on `input`, stores the raw string | `' 42 '` | | `.lazy` | syncs on `change` instead of `input` | `' 42 '`, written only once the edit is committed | | `.trim` | trims the string before writing | `'42'` | | `.number` | casts with `parseFloat`, keeps the string on `NaN` | `42` | They combine: `v-model.trim.number` trims first and then casts. In Vue 3 a text-like `v-model` accepts all three; `<select v-model>` accepts `.number` to cast the selected option values. ### `.lazy` A text field fires `change` when the user commits an edit — usually on blur or when pressing Enter — rather than per keystroke. With `.lazy` the model therefore updates less often, which suits fields whose value drives something expensive or noisy (a server-side availability check on the newsletter's email field, for example). The price is that any computed or preview bound to the model lags until the commit. ### `.trim` The model receives the trimmed string on every sync. While the field is focused Vue deliberately leaves the visible text alone, so the user can still type a space between two words; when the field fires `change`, Vue writes the trimmed value back into it. ### `.number` `.number` is the one people get wrong. Vue's cast is essentially: 1. Call `parseFloat(value)`. 2. If the result is a number, store the number. 3. If the result is `NaN`, store the **original string** unchanged. That produces some behaviour candidates do not expect: - **An empty field stores `''`.** `parseFloat('')` is `NaN`, so the empty string survives. The Vue guide points out this differs from the DOM's `valueAsNumber`, which would give `NaN`. - **Garbage stays a string.** In a plain text field, typing `abc` stores `'abc'`. - **A numeric prefix wins.** `parseFloat('42abc')` is `42`, so the model gets `42`, and on `change` Vue writes `42` back into the field. - **`type="number"` implies it.** Vue applies the `.number` cast automatically when the input's `type` is `number`, so `<input type="number" v-model="age">` stores numbers without the modifier. ## The newsletter age field The preferences form asks for an age to decide whether the subscriber may receive a particular edition. The naive version: ```vue <script setup lang="ts"> import { ref, computed } from 'vue' const age = ref<number | string>('') const canSubmit = computed(() => typeof age.value === 'number' && age.value >= 13) </script> <template> <input v-model.number="age" inputmode="numeric" /> </template> ``` Two points matter here: - The ref is typed `number | string`, because that is what `.number` really produces. Typing it as `number` hides the `''` and `'abc'` cases from the compiler. - The check tests `typeof age.value === 'number'` before comparing, so a cleared field is treated as "missing" rather than coerced into something that happens to pass or fail by accident. If the model must be `number | null`, convert explicitly — for example with a writable `computed` in front of the input, or by normalising on submit — rather than expecting the modifier to do it. ## Common misreadings - "`.lazy` syncs on blur" — it syncs on `change`, which for text fields often coincides with blur but also fires on Enter. - "`.number` guarantees a number" — it guarantees a number only when `parseFloat` succeeds. - "`.trim` rewrites the field on every keystroke" — only the model is trimmed during typing; the field is rewritten on `change`. - "Modifiers are free to use on checkboxes and radios" — those modes store booleans or bound values and do not take these string modifiers.
- In Vue 3, do you need .number on an input whose type is "number"?No. Vue's text `v-model` applies the `.number` cast automatically when the input's `type` is `number`, so the model receives numbers from valid input. It still follows the same rule: an empty field stays `''`, because `parseFloat('')` is `NaN` and Vue then keeps the original string.
- In Vue 3, what does v-model.number store if a user types 42abc into a plain text input?`42`. Vue casts with `parseFloat`, which reads the numeric prefix and ignores the rest, and on the `change` event Vue writes the cast value back, so the field then shows `42`. Only when no numeric prefix exists — `abc` — does the original string go into the model.
- When would you choose v-model.lazy on a Vue 3 text field?When each model change is costly or noisy — a watcher that validates the address against a server, analytics on edits, or a heavy computed. `.lazy` syncs on `change`, so the work runs once per committed edit instead of per keystroke. The trade-off is that live previews lag until the user commits.
saying these in an interview costs you the question
- v-model.number always stores a number, so clearing the field gives 0.
- .lazy makes v-model sync on the blur event specifically.
- .trim rewrites the visible text on every keystroke.
- type="number" inputs still need .number to get numbers into the model.
- An empty v-model.number field stores NaN, like valueAsNumber.