Pin Input

A row of single-character cells for OTP / verification codes.

Colors

Sizes

Masked (text mode)

Controlled + onComplete

Value: (empty)

Using Pin Input

Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.

Plain HTML — CSS classes

The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path ships no JavaScript, so Pin Input is styled but inert: its behavior comes from pin-input. For that behavior with no framework, use the node-tree path below.

/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";

Root class pin-input, with 14 modifiers:

  • .pin-input
  • .pin-input-cell
  • .pin-input-cell-accent
  • .pin-input-cell-error
  • .pin-input-cell-info
  • .pin-input-cell-lg
  • .pin-input-cell-md
  • .pin-input-cell-neutral
  • .pin-input-cell-primary
  • .pin-input-cell-secondary
  • .pin-input-cell-sm
  • .pin-input-cell-success
  • .pin-input-cell-warning
  • .pin-input-cell-xl
  • .pin-input-cell-xs

React

Typed components over those same classes, with behavior and accessibility from Base UI.

npm i @wizeworks/silicaui-react
import { PinInput } from "@wizeworks/silicaui-react";

Props

PropTypeRequiredDescription
lengthnumbernoNumber of cells. Default 6.
valuestringnoControlled value (a string up to length chars).
defaultValuestringnoUncontrolled initial value.
onValueChange(value: string) => voidnoFires with the next value on every keystroke, paste, or deletion.
onComplete(value: string) => voidnoFires once the value reaches length characters.
mode"numeric" | "text"noRestrict input to digits ("numeric", default) or any single character ("text").
maskbooleannoRender each cell's character as a dot, like a password field. Default false.
colorPinInputColornoAccent color; maps to pin-input-cell-<color> (border + focus ring).
sizePinInputSizenoDefault md. Matches same-size Input heights.
disabledbooleanno—
autoFocusbooleannoFocus the first cell on mount.
namestringnoField name for form submission (submits the joined value as one field).
"aria-label"stringno—

Source of the demo above

The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.

Show source
import { useState } from "react";
import { PinInput } from "@wizeworks/silicaui-react";
import { Section, Stack, Row } from "../lib/Section";
import { SIZES } from "../lib/data";

export function PinInputDemo() {
    const [code, setCode] = useState("");
    const [completed, setCompleted] = useState("");

    return (
        <>
            <Section title="Colors">
                <Stack>
                    <PinInput length={4} />
                    <PinInput length={4} color="primary" />
                    <PinInput length={4} color="error" defaultValue="12" />
                    <PinInput length={4} disabled defaultValue="1234" />
                </Stack>
            </Section>

            <Section title="Sizes">
                <Stack>
                    {SIZES.map((size) => (
                        <PinInput key={size} length={4} size={size} />
                    ))}
                </Stack>
            </Section>

            <Section title="Masked (text mode)">
                <Row>
                    <PinInput length={4} mode="text" mask />
                </Row>
            </Section>

            <Section title="Controlled + onComplete">
                <Stack>
                    <PinInput
                        length={6}
                        value={code}
                        onValueChange={setCode}
                        onComplete={setCompleted}
                    />
                    <p className="text-xs opacity-60">
                        Value: {code || "(empty)"}
                        {completed && ` — completed: ${completed}`}
                    </p>
                </Stack>
            </Section>
        </>
    );
}

Vanilla — node tree to HTML

pin-input — real single-char <input> cells (index-based), unlike date-segment's buffer-accumulate model — see pin-input.ts. Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.

npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviors
import { atom, toHtml } from "@wizeworks/silicaui-html";

const node = atom("PinInput", "pin-input", { /* props */ });
const markup = toHtml(node);

Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "pin-input pin-input-primary" rather than passing a colour as a prop.

Takes children
no
Hydrated by
pin-input

Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.

The full API for Pin Input, on every path, is available through the Silica MCP server.