Code input
Six boxes for a one-time code: two-step sign-in, and the step-up check before a sensitive change. It reads as one field; typing or pasting fills the boxes in order.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { CodeInput } from '@/ui/code-input';<CodeInput />Two-step sign-in
Say where the code comes from above the boxes. The sixth digit verifies; the boxes wait at 50% and Verify shows its spinner while the code is checked. A wrong code clears the boxes, turns their edges red and says what to do; five wrong codes lock it for 15 minutes. Try 481902, then any other code.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { CodeInput } from '@/ui/code-input';
import { Field, FieldDescription, FieldError, FieldLabel } from '@/ui/field';
import { Link } from '@/ui/link';
// Two-step sign-in (on the real page the boxes take focus on load: autoFocus). 481902 is the right code here; anything else is refused. Five wrong codes lock it for 15 minutes.
export default function TwoStep() {
const [code, setCode] = useState('');
const [error, setError] = useState('');
const [checking, setChecking] = useState(false);
const [wrong, setWrong] = useState(0);
const [signedIn, setSignedIn] = useState(false);
const locked = wrong >= 5;
async function verify(entered: string) {
if (checking || locked || entered.length < 6) return; // A second Enter while checking does nothing.
setChecking(true);
await new Promise((resolve) => setTimeout(resolve, 800));
setChecking(false);
if (entered === '481902') return setSignedIn(true);
const tries = wrong + 1;
setWrong(tries);
setCode('');
setError(
tries >= 5
? 'Too many wrong codes. Wait 15 minutes, then try again.'
: 'That code didn’t work. Enter the current code from your authenticator app.',
);
}
return (
<form
className="flex w-full max-w-80 flex-col gap-200"
onSubmit={(event) => {
event.preventDefault();
void verify(code);
}}
>
<Field invalid={!!error}>
<FieldLabel htmlFor="mfa-code" visuallyHidden>
Authenticator code
</FieldLabel>
<FieldDescription id="mfa-code-help">Enter the current code from your authenticator app.</FieldDescription>
<CodeInput
id="mfa-code"
value={code}
onValueChange={(next) => {
setCode(next);
if (next) setError('');
}}
onComplete={verify}
invalid={!!error}
disabled={checking || locked || signedIn}
aria-describedby={error ? 'mfa-code-help mfa-code-error' : 'mfa-code-help'}
/>
<FieldError id="mfa-code-error">{error}</FieldError>
</Field>
<div className="flex items-center gap-200">
<Button type="submit" variant="primary" loading={checking} loadingLabel="Verifying" disabled={locked || signedIn || code.length < 6}>
{signedIn ? 'Signed in' : 'Verify'}
</Button>
<Link href="#backup-code" variant="subtle">
Use a backup code instead
</Link>
</div>
</form>
);
}States
Empty before focus, with no placeholder digits. Filled, being checked at 50%, and refused with every box on the danger edge.
import { CodeInput } from '@/ui/code-input';
// Empty before focus, filled, being checked, and refused. Each is named for screen readers.
export default function States() {
return (
<div className="flex flex-col gap-200">
<CodeInput aria-label="Empty code" />
<CodeInput aria-label="Filled code" value="481902" />
<CodeInput aria-label="Code being checked" value="481902" disabled />
<CodeInput aria-label="Refused code" value="481907" invalid />
</div>
);
}Other lengths
length sets the number of boxes, such as a four-digit code from an email. A paste of "4 8 1 9" or "48-19" drops the separators and fills every box.
We sent it to joseph@northbridge.com. It works for 10 minutes.
import { useState } from 'react';
import { CodeInput } from '@/ui/code-input';
import { Field, FieldDescription, FieldLabel } from '@/ui/field';
// The step-up check before a sensitive change, with a four-digit code. Paste "4 8 1 9" or "48-19": separators drop.
export default function StepUp() {
const [code, setCode] = useState('');
return (
<Field className="w-fit">
<FieldLabel htmlFor="step-up-code">Code from your email</FieldLabel>
<CodeInput id="step-up-code" length={4} value={code} onValueChange={setCode} aria-describedby="step-up-help" />
<FieldDescription id="step-up-help">We sent it to joseph@northbridge.com. It works for 10 minutes.</FieldDescription>
</Field>
);
}Undo control
Undo is how Ascern recovers from a write: an Undo on the toast that confirms it, ⌘Z for the newest one, and an inline Undo on a receipt or activity line for changes Flo made.
Color swatch picker
Picks a colour from a fixed set, for a mail tag or a select option. People pick a hue to tell things apart; they never type a hex.