Raccourci clavier : Ctrl + K
Commencer

Code à usage unique

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.

Utilisation

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VInputOTP } from 'vectis-ui'

const code = ref('')
</script>

<template>
  <VInputOTP v-model="code" :length="6" label="Verification code" />
</template>

Exemples

Libellé et aide

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é.

vue

4, a PIN

6, the default

8, a backup code

<script setup lang="ts">
import { VInputOTP, VTypography } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <div class="row">
      <VInputOTP :length="4" label="Four digit code" />
      <VTypography variant="caption" tone="muted">4, a PIN</VTypography>
    </div>

    <div class="row">
      <VInputOTP :length="6" label="Six digit code" />
      <VTypography variant="caption" tone="muted">6, the default</VTypography>
    </div>

    <div class="row">
      <VInputOTP :length="8" label="Eight digit code" />
      <VTypography variant="caption" tone="muted">8, a backup code</VTypography>
    </div>
  </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-4);
}
</style>

Formats

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

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.

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

Submitting with empty boxes is refused

<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

PropTypeDéfaut
lengthnumber6
Combien de cases compte le code. Ignorée dès qu'un pattern est donné.
formatInputOTPFormat'numeric' | 'alpha' | 'alphanumeric''numeric'
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.
patternstringaucune
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.
separatorIconIconSourceaucune
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.
sizeInputOTPSize'sm' | 'md' | 'lg''md'
La taille des cases : 32, 40 ou 48 pixels.
compactbooleanfalse
Retire 4px aux cases, en laissant le texte et les icônes tels quels.
disabledbooleanfalse
Rend toutes les cases inutilisables, grisées par les tokens de couleur.
readonlybooleanfalse
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.
invalidbooleanfalse
Marque le code comme erroné, ce qui colore les cases et le signale aux technologies d'assistance.
labelstringaucune
Ce que les lecteurs d'écran annoncent pour la rangée dans son ensemble. Il retombe sur le dictionnaire du design system.
hintstringaucune
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-modelstring''
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énementType
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.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender