Base components
Verification code
One box per digit for a 4 or 6 digit code, where each digit rises into its box as it is typed, focus moves to the next box, and a pasted code fills the boxes one after another.
Live preview
Loading preview
A 6 digit code at size md with the hint "Enter the code from your email." and a Send again link, a 4 digit code at size sm, and an invalid state. Replay pastes 482913 so the digits fill one after another, then clears them.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add verification-codeUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { Button } from '../button/button';
import { VerificationCode } from './verification-code';
const PASTED = '482913';
const HOLD_MS = 1600;
export default function VerificationCodeDemo({ replay = 0 }: { replay?: number }) {
const [code, setCode] = React.useState('');
React.useEffect(() => {
if (replay === 0) return;
// Pastes a whole code at once, so the digits fill one box after another, then clears it.
setCode(PASTED);
const timer = window.setTimeout(() => setCode(''), HOLD_MS);
return () => window.clearTimeout(timer);
}, [replay]);
return (
<div className="flex flex-col items-center gap-10">
<div className="flex flex-col items-start gap-3">
<VerificationCode value={code} onValueChange={setCode} hint="Enter the code from your email." />
<Button hierarchy="link-color" size="sm" onClick={() => setCode('')}>
Send again
</Button>
</div>
<div className="flex flex-wrap items-start justify-center gap-10">
<VerificationCode length={4} size="sm" label="Sign-in code" />
<VerificationCode
length={4}
size="sm"
label="Card code"
defaultValue="1234"
invalid
hint="This code is not correct. Check the code and try again."
/>
</div>
</div>
);
}
API
Parts: root (field), label, digit boxes (4 or 6), separator dash (after the third box of 6), caret (in the focused empty box), hint text (becomes the error text when invalid).
| Prop | Type | Default | Description |
|---|---|---|---|
| length | 4 | 6 | 6 | Number of digit boxes. |
| value | string | undefined | Controlled code. |
| defaultValue | string | "" | Code when uncontrolled. |
| size | "sm" | "md" | "lg" | "md" | Boxes of 64, 80 or 96px. |
| label | string | "Verification code" | Label above the boxes. |
| hint | string | undefined | Help line under the boxes. When invalid, pass the error text here. |
| invalid | boolean | false | Error border and error digits in every box, and a red hint. |
| disabled | boolean | false | Turns every box off. |
| autoFocus | boolean | false | Focuses the first empty box on mount. |
| name | string | undefined | Form field name; the hidden input holds the whole code. |
| Event | Description |
|---|---|
| valueChange | The code changed. Payload: the new code. |
| complete | Every box has a digit. Payload: the full code. |
| Slot | Description |
|---|---|
| hint | Rich hint content, for example a Send again link. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| VC1 | hover a box | box border | border-color | border-control → border-control_hover | duration-sm (200)ease-out | Swaps at once |
| VC2 | focus moves to a box | box | border-color, box-shadow | border-control, shadow-xs → border-brand, focus-ring-shadow-xs | duration-2xs (120)ease-out | Shows at once |
| VC3 | a digit is typed | digit | opacity, translate y | 0, distance-rise → 1, 0 | duration-sm (200)ease-out | Shows at once |
| VC4 | a digit is typed | box border | border-color | border-control → border-brand | duration-sm (200)ease-out | Swaps at once |
| VC5 | a digit is deleted | digit | opacity | 1 → 0 | duration-2xs (120)ease-exit | Hides at once |
| VC6 | a code is pasted or filled by the browser | digits | opacity, translate y (as VC3), one box after another | 0, distance-rise → 1, 0 | duration-sm (200)ease-outdelay stagger-list x box | Shows at once |
| VC7 | while a box is focused and empty | caret | opacity | 1 → 0, repeats | duration-6xl (900)ease-linear | No loop |
| VC8 | invalid -> true | box borders, digits, hint | border-color, color | border-brand, text-brand-tertiary_alt, text-tertiary → border-error, text-error-primary, text-error-primary | duration-sm (200)ease-out | Swaps at once |
Accessibility
- Built on a one-time code primitive: one input per box with inputmode numeric and autocomplete one-time-code on the first box, so phones can fill the code from a message.
- Each box is named "Digit 1 of 6" and the group is labelled by the label; the hint is linked with aria-describedby.
- Typing moves focus to the next box, Backspace clears and moves back, Left and Right move between boxes, and paste fills from the focused box.
- Placeholder digits (a faded 0) are aria-hidden.
- invalid sets aria-invalid on every box; the hint says what to do, for example "This code is not correct. Check the code and try again."
- No shake on error: the error colors and the hint carry it, so nothing moves under the pointer.
- Reduced motion: digits show at once and the caret does not blink.