Vue
The v-mask directive from tinyval-mask/vue puts a mask on a field. The package doesn't need Vue at runtime: the directive is a plain object, Vue is used only for types.
<script setup lang="ts">
import { ref } from 'vue';
import { phone, money } from 'tinyval-mask/presets';
import { vMask } from 'tinyval-mask/vue';
// create masks once, not inline in the template
const phoneMask = phone();
const moneyMask = money({ currency: '₽' });
const tel = ref('');
const amount = ref('');
</script>
<template>
<input v-model="tel" v-mask="phoneMask" />
<input v-model="amount" v-mask="moneyMask" inputmode="decimal" />
<input v-mask="'99.99.9999'" />
</template>v-modelgets the masked value:+7 (999) 123-45-67. To get the unmasked value add the.rawmodifier — see below.- Create masks in
<script>: an expression likev-mask="phone()"creates a new mask on every render. - The directive can go on a wrapper — it finds the
<input>inside. That's how masks work with UI library fields. - Global registration:
app.directive('mask', vMask).
Unmasked value — .raw
With the .raw modifier the field shows the mask while v-model gets the value without it — the same as unmask returns. It can be saved right away:
<input v-model="tel" v-mask.raw="phoneMask" /> <!-- tel: '79991234567' -->
<input v-model="amount" v-mask.raw="moneyMask" /> <!-- amount: '1500.5' -->A value from the model is shown with the mask: tel = '79991234567' → the field shows +7 (999) 123-45-67. The caret doesn't jump when editing in the middle, even when Vue re-renders the field.
Visible template
Pass a placeholder character — on focus the field shows the whole template and the caret goes to the first empty position:
<input v-model="tel" v-mask.raw="{ mask: phoneMask, placeholder: '_' }" placeholder="Phone" />
<!-- on focus: +7 (___) ___-__-__ typing: +7 (999) 12_-__-__ -->- The template doesn't go into
v-model: without.rawit holds+7 (999) 12, with.raw—7999912. - An empty field is cleared on blur — the regular
placeholderhint is shown. A partially filled one keeps the template. - Number masks have no template: their length isn't fixed.
With tinyval-vue
v-mask works on <TvField> and on any tinyval-vue form field. With .raw the form gets the digits right away, and the schema validates them without unmask:
<script setup lang="ts">
import { object, string, pipe, length } from 'tinyval';
import { useForm, TvForm, TvField, TvError } from 'tinyval-vue';
import { phone } from 'tinyval-mask/presets';
import { vMask } from 'tinyval-mask/vue';
const phoneMask = phone();
const Contact = object({
phone: pipe(string(), length(11)),
});
const form = useForm(Contact, { initial: { phone: '' } });
</script>
<template>
<TvForm :form="form" @submit="(data) => api.post('/contacts', data)">
<TvField name="phone" v-mask.raw="{ mask: phoneMask, placeholder: '_' }" inputmode="tel" />
<TvError name="phone" />
</TvForm>
</template>The field shows +7 (999) 123-45-67; form.values.phone and data have 79991234567.
UI libraries
<el-input v-model="tel" v-mask="phoneMask" />
<v-text-field v-model="tel" v-mask="phoneMask" />