Form validation
Two fields and a checkbox whose Next control is guarded by the page’s own check: a bad email or a short password is refused with an error under the instruction, and an unticked box with the hole alone.
const story = { id: 'form-validation', steps: [ { id: 'email', target: { elements: 'input[name="email"]', interactive: true }, message: 'Enter the address you want to sign in with.', validate: (el) => /.+@.+\..+/.test((el as HTMLInputElement).value), // Under the instruction rather than over it. Pressing Next with an // empty field used to leave the step asking for nothing. error: 'That does not look like an email address yet.', }, { id: 'password', target: { elements: 'input[name="password"]', interactive: true }, message: 'Pick a password of at least eight characters.', validate: (el) => (el as HTMLInputElement).value.length >= 8, // Worked out from what was typed, which is what the function form is // for. `types.ts` says when it is asked. error: (el) => `Eight characters at least, and that is ${(el as HTMLInputElement).value.length}.`, }, // No `error`, so the hole is the only answer this refusal has: it shakes, // or under prefers-reduced-motion dims where it stands and opens again. // Watch it by pressing Next with the box unticked, at ×1 and under // prefers-reduced-motion. DESIGN.md, **A failed attempt**. { id: 'terms', target: { elements: '[data-terms]', interactive: true }, message: 'Agree to the terms before the account is made.', validate: (el) => el.querySelector<HTMLInputElement>('input')?.checked === true, }, { id: 'submit', target: { elements: 'button[type="submit"]', interactive: true }, message: 'Now create the account.', }, ],} satisfies LekoStory