sveltelean Primitives
Versionv0.2.0 GitHub

Example

Six-digit code

Enter the six digits sent to your phone.

<label for="otp-field-code">Verification code</label>
<input
	id="otp-field-code"
	name="code"
	data-slean="otp-field"
	inputmode="numeric"
	autocomplete="one-time-code"
	maxlength="6"
	pattern="\d{6}"
	required
	aria-describedby="otp-field-code-hint"
/>
<p id="otp-field-code-hint">Enter the six digits sent to your phone.</p>
Tier 0: the Svelte source adds the stylesheet import and writes the pattern as a string, because braces start an expression in a .svelte attribute. Type, paste or delete: the cells are the input’s background, so no focus moves between boxes. Everything here works with page JavaScript disabled.

Why this implementation exists

A code input built from one input per digit needs JavaScript on every keystroke to move focus, and more to rebuild what a single text field does on its own: a pasted or autofilled code has to be split across the boxes, Backspace and selection have to cross them, and a screen reader meets six unlabeled fields.

Here the code is one <input>. autocomplete="one-time-code" lets the browser offer a code it received, inputmode="numeric" brings up a digit keyboard, and maxlength with pattern constrain the value. The stylesheet draws the cells: a monospace font gives every character the same advance, letter-spacing spaces the characters evenly, and a gradient draws one mark under each position.

The browser owns

  • typing, deleting, selecting and pasting in one field
  • the numeric keyboard (inputmode) and the offered code (autocomplete="one-time-code")
  • maxlength, pattern and required validation, :user-invalid after an edit
  • form participation and the label association

Svelte Lean owns

  • otp-field.css: the cells, drawn with a monospace font, letter-spacing and a gradient for 4, 6 or 8 characters
  • the contract and the OtpFieldLength type
  • documentation

Usage

The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.

npm install @svelte-lean/styles
stylesheets
import '@svelte-lean/styles/otp-field.css';

Set maxlength to the length of the code and data-slean-length to the same number: 4, 6 or 8, and 6 when the attribute is absent. Match pattern to the characters you send, \d{6} for six digits.

In a .svelte file, braces in an attribute value start an expression, so write the pattern as a string: pattern={'\\d{6}'}.

The browser keeps a pasted code as pasted, up to maxlength: spaces and dashes stay and fail pattern. Send codes without separators, or handle the paste in the application.

Anatomy

PartElementdata-slean-partRequiredNotes
root<input data-slean="otp-field" inputmode="numeric" autocomplete="one-time-code" maxlength="6" pattern="\d{6}">–yesOne input for the whole code, with a label. data-slean-length="4" or "8" when maxlength is 4 or 8; the default is 6.
cellsthe input’s background–styles onlyDrawn by the stylesheet, not elements: one mark per character position, accent-colored while the field has focus.

Runtime profile

Tier 0: the field has no behavior module, and the Vite plugin maps otp-field to no module. The cells are CSS; typing, pasting and autofill run in the browser.

Accessibility contract

  • One labelled text input: assistive technology announces one field with its label and hint, not one box per digit.
  • aria-describedby points at the hint, and at the error message when there is one.
  • The focus ring is the shared one; while the field has :focus-visible the stylesheet also draws the cell marks in the accent color.
  • :user-invalid marks the field only after the user edited it, so an untouched required field is not red.

Keyboard

KeyWhenResult
charactersfocus in the fieldTyped at the caret; typing stops at maxlength (native)
Backspace/Deletefocus in the fieldRemoves a character; no focus moves between cells (native)
ArrowLeft/ArrowRight/Home/Endfocus in the fieldMoves the caret (native)
Enterfocus in the fieldSubmits the form through implicit submission (native)

Platform features

FeatureBaselineOutside the target
inputmodeBaseline 2021The default keyboard
autocomplete="one-time-code"Safari on iOS and macOS offers a code received by SMS; elsewhere browser-dependentA plain text input; the code is typed or pasted
:user-invalidBaseline 2023No invalid border after an edit

Without JavaScript

Fully functional: typing, pasting, autofill, validation and submission need no script.

Server rendering

Static HTML. A code field has no initial value; the server renders the empty input.

Before hydration

Nothing is attached; the field works before and after hydration alike.

Styling

otp-field.css sets a monospace font and letter-spacing, sizes the content box to the width the characters take so the input never scrolls, and draws the marks with a gradient in the border token (the accent token while focused). The field is drawn left to right in every document direction, because the geometry starts at the first character. The tokens it reads:

TokenDefault (light)Applies to
--slean-control-height-lg2.75remblock-size
--slean-space-20.5remblock-size, background-position
--slean-control-bordervar(--slean-border-strong)border, background-image
--slean-radius-md0.625remborder-radius
--slean-control-bgvar(--slean-surface)background-color
--slean-fgvar(--slean-neutral-12)color
--slean-accentoklch(54% 0.19 258)caret-color, background-image
--slean-font-monoui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospacefont-family
--slean-text-lg1.125remfont-size
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-control-border-hovervar(--slean-accent)border-color
--slean-control-border-focusvar(--slean-accent)border-color
--slean-control-ring0 0 0 3px color-mix(in oklch, var(--slean-accent) 22%, transparent)box-shadow
--slean-warningoklch(76% 0.16 80)border-color
--slean-control-ring-warning0 0 0 3px color-mix(in oklch, var(--slean-warning) 28%, transparent)box-shadow
--slean-dangeroklch(55% 0.2 25)border-color
--slean-control-ring-danger0 0 0 3px color-mix(in oklch, var(--slean-danger) 22%, transparent)box-shadow
--slean-control-border-disabledvar(--slean-border)border-color
--slean-control-bg-disabledvar(--slean-muted)background-color

State selectors the stylesheet targets, all from the platform or ARIA: ::placeholder, :disabled, :focus-visible, :hover, :user-invalid, [aria-invalid="true"].

Variant attributes: data-slean-length (4, 8); data-status (warning, error).

Controlled integration

Submitting when the last digit arrives is the application’s decision; the field does not do it. With bind:value, an effect can submit the form once the value matches.

verify.svelte
<script lang="ts">
	let form: HTMLFormElement;
	let code = $state('');

	// The field never submits by itself. This application submits once six digits are in.
	$effect(() => {
		if (/^\d{6}$/.test(code)) form.requestSubmit();
	});
</script>

<form method="post" action="/verify" bind:this={form}>
	<label for="otp-field-code">Verification code</label>
	<input id="otp-field-code" name="code" data-slean="otp-field" inputmode="numeric"
		autocomplete="one-time-code" maxlength="6" pattern={'\\d{6}'} required bind:value={code} />
</form>

Compatibility notes

inputmode is Baseline 2021; without it the default keyboard appears. Safari on iOS and macOS offers a code received by SMS for autocomplete="one-time-code"; elsewhere the suggestion depends on the browser and the keyboard. With a maxlength other than 4, 6 or 8 the stylesheet draws six cells and the input still works.

Examples

Lengths

data-slean-length draws four or eight cells and must equal maxlength. The second field takes letters and digits: it has no inputmode, a pattern for both, and spellcheck="false" so no spelling marks cross the cells.

lengths
<!-- data-slean-length matches maxlength: 4, 6 (the default) or 8. -->
<label for="otp-field-short">Four-digit code</label>
<input id="otp-field-short" name="short-code" data-slean="otp-field" data-slean-length="4"
	inputmode="numeric" autocomplete="one-time-code" maxlength="4" pattern="\d{4}" />

<!-- Letters and digits: no inputmode, a pattern for both, no spelling marks. -->
<label for="otp-field-long">Eight-character code</label>
<input id="otp-field-long" name="long-code" data-slean="otp-field" data-slean-length="8"
	autocomplete="one-time-code" maxlength="8" pattern="[A-Za-z0-9]{8}" spellcheck="false" />

Testing

Source