Un code saisi un caractère par case : un mot de passe à usage unique, une clé de licence, une référence. Un collage remplit toute la rangée, et la valeur ne contient que les caractères, jamais les séparateurs.
label n'affiche rien ici : il nomme la rangée pour les technologies d'assistance. hint est le texte que voit le lecteur, lié à la rangée pour être lu avec le libellé.
vue
Check your phone
We sent a six digit code to +33 6 12 34 56 78.
The code lasts ten minutes. Ask for another one if it has expired.
<script setup lang="ts">
import { ref } from 'vue'
import { VInputOTP, VTypography } from 'vectis-ui'
const code = ref('')
</script>
<template>
<div class="column">
<!-- `label` names the row for assistive technology and renders nothing: the
instructions above are the page's own, written where they read best. -->
<VTypography as="h3" variant="heading-4">Check your phone</VTypography>
<VTypography tone="muted">We sent a six digit code to +33 6 12 34 56 78.</VTypography>
<VInputOTP
v-model="code"
:length="6"
label="Verification code"
hint="The code lasts ten minutes. Ask for another one if it has expired."
/>
</div>
</template>
<style scoped>
.column {
display: grid;
justify-items: start;
gap: var(--vectis-space-3);
}
</style>
Tailles
size définit la hauteur à 32, 40 ou 48 pixels, et compact lui retire 4px. Le caractère à l'intérieur est agrandi d'un ou deux crans au-dessus du palier de la rangée.
vue
sm
md
lg
sm compact
md compact
lg compact
<script setup lang="ts">
import { VInputOTP, VTypography } from 'vectis-ui'
const sizes = ['sm', 'md', 'lg'] as const
</script>
<template>
<div class="column">
<div v-for="size in sizes" :key="size" class="row">
<VInputOTP :length="4" :size="size" :label="`Code, ${size}`" />
<VTypography variant="caption" tone="muted">{{ size }}</VTypography>
</div>
<!-- Compact takes 4px off the boxes and leaves the text where it was. -->
<div v-for="size in sizes" :key="`${size}-compact`" class="row">
<VInputOTP :length="4" :size="size" compact :label="`Code, ${size} compact`" />
<VTypography variant="caption" tone="muted">{{ size }} compact</VTypography>
</div>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-4);
}
.row {
display: flex;
align-items: center;
gap: var(--vectis-space-4);
}
</style>
Longueur
length est le nombre de cases du code, six par défaut. Elle est ignorée dès qu'un pattern est donné.
format décide des caractères dont le code est fait, filtre ce qui peut être saisi ou collé et choisit le clavier qu'offre un téléphone. Hors code numérique, la valeur est forcée en majuscules.
vue
Digits only, and a phone offers its number pad
Letters only, typed in either case and kept in capitals
Letters and digits, again in capitals
<script setup lang="ts">
import { ref } from 'vue'
import { VInputOTP } from 'vectis-ui'
const pin = ref('')
const word = ref('')
const key = ref('')
</script>
<template>
<div class="column">
<VInputOTP
v-model="pin"
:length="6"
format="numeric"
label="Numeric code"
hint="Digits only, and a phone offers its number pad"
/>
<!-- Outside a numeric code the value is forced to capitals, so it has one
canonical form whatever the reader typed. -->
<VInputOTP
v-model="word"
:length="5"
format="alpha"
label="Letter code"
hint="Letters only, typed in either case and kept in capitals"
/>
<VInputOTP
v-model="key"
:length="6"
format="alphanumeric"
label="Licence key"
hint="Letters and digits, again in capitals"
/>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
</style>
Gabarit
pattern écrit la forme du code : chaque # est une case à remplir et tout autre caractère un littéral dessiné entre les cases, jamais saisi et jamais compris dans la valeur. Il l'emporte sur length.
vue
-
Two groups of three. Value: empty
GT-
Three characters after the prefix. Value: empty
..
Nine digits in three groups. Value: empty
<script setup lang="ts">
import { ref } from 'vue'
import { VInputOTP } from 'vectis-ui'
const grouped = ref('')
const prefixed = ref('')
const reference = ref('')
</script>
<template>
<div class="column">
<!-- Each # is a box, everything else is drawn between them and never enters the
value. The pattern wins over `length`, which is then ignored. -->
<VInputOTP
v-model="grouped"
pattern="###-###"
label="Grouped code"
:hint="`Two groups of three. Value: ${grouped || 'empty'}`"
/>
<!-- A literal can be a real prefix rather than punctuation. It is shown, never
typed, and stays out of the value. -->
<VInputOTP
v-model="prefixed"
pattern="GT-###"
format="alphanumeric"
label="Ticket reference"
:hint="`Three characters after the prefix. Value: ${prefixed || 'empty'}`"
/>
<VInputOTP
v-model="reference"
pattern="###.###.###"
label="Payment reference"
:hint="`Nine digits in three groups. Value: ${reference || 'empty'}`"
/>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
</style>
Séparateurs
separatorIcon remplace les littéraux dessinés par un pattern, tous sans exception : elle convient donc à un gabarit dont les séparateurs ne sont que de la ponctuation.
vue
-
The literal, as written
One icon in place of the dash
Both literals take the same icon
<script setup lang="ts">
import { VInputOTP } from 'vectis-ui'
import { chevron_right as chevronRight, close } from 'vectis-ui/icons'
</script>
<template>
<div class="column">
<!-- Left alone, a literal is drawn as the character the pattern names. -->
<VInputOTP pattern="###-###" label="Grouped code" hint="The literal, as written" />
<!-- The icon replaces EVERY literal of the pattern, so it suits one whose
separators are punctuation and nothing more. -->
<VInputOTP
pattern="###-###"
:separator-icon="chevronRight"
label="Grouped code with a chevron"
hint="One icon in place of the dash"
/>
<VInputOTP
pattern="###.###.###"
:separator-icon="close"
label="Payment reference with a cross"
hint="Both literals take the same icon"
/>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
</style>
Collage et remplissage automatique
Un code collé n'importe où dans la rangée est réparti sur toutes les cases, les littéraux du pattern étant absorbés avec lui. La première case porte autocomplete="one-time-code", si bien qu'un code venu d'un SMS ou d'un gestionnaire de mots de passe est réparti de la même façon.
vue
Copy GT-4F2, then paste it anywhere in the row.
GT-
Value: empty
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VInputOTP, VTypography } from 'vectis-ui'
const SAMPLE = 'GT-4F2'
const code = ref('')
const copied = ref(false)
/* A plain clipboard write from a click handler, so nothing touches the browser
outside an event. */
async function copy() {
await navigator.clipboard.writeText(SAMPLE)
copied.value = true
}
</script>
<template>
<div class="column">
<div class="row">
<VTypography as="span"
>Copy <code>{{ SAMPLE }}</code
>, then paste it anywhere in the row.</VTypography
>
<VButton variant="outline" tone="neutral" size="sm" @click="copy">
{{ copied ? 'Copied' : 'Copy' }}
</VButton>
</div>
<!-- The paste is spread across the boxes and the prefix is consumed with it: a
code copied in its formatted form lands as three characters. -->
<VInputOTP
v-model="code"
pattern="GT-###"
format="alphanumeric"
label="Ticket reference"
:hint="`Value: ${code || 'empty'}`"
/>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-4);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--vectis-space-3);
}
</style>
Lire le code
La valeur est une seule chaîne des caractères seuls, jamais des séparateurs. Les cases remplies partent toujours de la première : taper dans une case au-delà de la première case vide remplit cette case vide, et vider une case ramène les caractères suivants en arrière. complete est émis quand le code devient complet, avec le code terminé.
vue
Try 481902
Value: empty (0 of 6)
<script setup lang="ts">
import { ref } from 'vue'
import { VInputOTP, VTypography } from 'vectis-ui'
const EXPECTED = '481902'
const code = ref('')
const verdict = ref<'right' | 'wrong' | null>(null)
/* `complete` fires once every box is filled, which is the cue to verify rather than
something to work out from the value's length. */
function verify(value: string) {
verdict.value = value === EXPECTED ? 'right' : 'wrong'
}
</script>
<template>
<div class="column">
<VInputOTP
v-model="code"
:length="6"
:invalid="verdict === 'wrong'"
label="Verification code"
hint="Try 481902"
@complete="verify"
@update:model-value="verdict = null"
/>
<!-- The value is the characters alone and is shorter than the row while it is
being typed. -->
<VTypography variant="body-sm" tone="muted">
Value: {{ code || 'empty' }} ({{ code.length }} of 6)
</VTypography>
<VTypography v-if="verdict === 'right'" variant="body-sm" tone="success">
The code matches.
</VTypography>
<VTypography v-else-if="verdict === 'wrong'" variant="body-sm" tone="danger">
That code is not the one we sent.
</VTypography>
</div>
</template>
<style scoped>
.column {
display: grid;
justify-items: start;
gap: var(--vectis-space-3);
}
</style>
Dans un formulaire
La rangée se soumet comme n'importe quel champ natif. name, form et required atteignent un input masqué qui porte le code, et un code qui ne remplit pas toutes les cases est invalide : le navigateur refuse de soumettre le formulaire.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VInputOTP, VTypography } from 'vectis-ui'
const code = ref('')
const submitted = ref<string | null>(null)
/* The form reads the code through `name`, as it would from any native field. A code
that does not fill every box is invalid, so the browser refuses to submit it. */
function onSubmit(event: Event) {
const data = new FormData(event.target as HTMLFormElement)
submitted.value = String(data.get('code'))
}
</script>
<template>
<form class="column" @submit.prevent="onSubmit">
<VInputOTP
v-model="code"
name="code"
required
:length="6"
label="Verification code"
hint="Submitting with empty boxes is refused"
/>
<VButton type="submit">Verify</VButton>
<VTypography v-if="submitted" variant="body-sm" tone="muted">
The form sent code={{ submitted }}
</VTypography>
</form>
</template>
<style scoped>
.column {
display: grid;
justify-items: start;
gap: var(--vectis-space-3);
}
</style>
États
disabled met toute la rangée hors de portée, grisée par les tokens de couleur. readonly montre le code figé pendant que les cases gardent le focus et restent copiables. invalid colore les cases et indique aux technologies d'assistance que le code a été refusé.
vue
Out of reach entirely
Shown, and copyable, but not editable
That code is not the one we sent
<script setup lang="ts">
import { VInputOTP } from 'vectis-ui'
</script>
<template>
<div class="column">
<VInputOTP
model-value="481902"
:length="6"
disabled
label="Disabled code"
hint="Out of reach entirely"
/>
<!-- Frozen rather than out of reach: the boxes still take the focus and the code
can be selected and copied. -->
<VInputOTP
model-value="481902"
:length="6"
readonly
label="Read-only code"
hint="Shown, and copyable, but not editable"
/>
<VInputOTP
model-value="481900"
:length="6"
invalid
label="Rejected code"
hint="That code is not the one we sent"
/>
</div>
</template>
<style scoped>
.column {
display: grid;
gap: var(--vectis-space-5);
}
</style>
API
Props
Prop
Type
Défaut
length
number
6
Combien de cases compte le code. Ignorée dès qu'un pattern est donné.
De quels caractères le code est fait. Cela filtre ce qui peut être saisi ou collé, et décide du clavier qu'un téléphone propose.
pattern
string
aucune
La forme du code : chaque # est une case à remplir, et tout autre caractère est un séparateur affiché entre les cases sans jamais faire partie de la valeur, 'GT-###' ou '###.###.###'. Il l'emporte sur length.
Une icône dessinée à la place de chaque séparateur du motif. Elle convient à un gabarit dont les séparateurs sont purement décoratifs, '###-###', et non à un gabarit portant du texte porteur de sens comme 'GT-###', que l'icône effacerait.
size
InputOTPSize'sm' | 'md' | 'lg'
'md'
La taille des cases : 32, 40 ou 48 pixels.
compact
boolean
false
Retire 4px aux cases, en laissant le texte et les icônes tels quels.
disabled
boolean
false
Rend toutes les cases inutilisables, grisées par les tokens de couleur.
readonly
boolean
false
Affiche le code sans permettre de le changer. Les cases gardent leur focus et le code reste sélectionnable et copiable, ce qui le distingue de disabled.
invalid
boolean
false
Marque le code comme erroné, ce qui colore les cases et le signale aux technologies d'assistance.
label
string
aucune
Ce que les lecteurs d'écran annoncent pour la rangée dans son ensemble. Il retombe sur le dictionnaire du design system.
hint
string
aucune
Une ligne d'aide sous les cases, pour dire où le code a été envoyé ou combien de temps il vaut. Elle est liée à la rangée pour les technologies d'assistance, donc lue avec le libellé. Contrairement à label, qui nomme la rangée sans rien afficher, ce texte est visible.
v-model
string
''
Le code en une seule chaîne, sans les séparateurs : un gabarit GT-### donne tout de même trois caractères. Il est vide au départ, et plus court que la longueur complète pendant la saisie.
Événements
Événement
Type
complete
[code: string]
Le code vient de devenir complet, avec le code terminé. C'est le signal pour le vérifier. Retaper un caractère d'un code complet à l'identique ne le réémet pas.
Types
Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.