Input
A thin, ref-forwarding wrapper around the native <input> element. The component owns the visual surface — border, padding, height, focus-ring, invalid-state, file-slot typography — and forwards every other native attribute (type, value, onChange, placeholder, name, disabled, readOnly, pattern, inputMode, autoComplete, …) verbatim. Three CVA variants × three sizes × every native type covers the full single-line input surface without a per-type wrapper.
Installation
npx shadcn@latest add https://craftbits.dev/r/input.jsonUsage
Controlled — pair value with onChange so a parent owns the text:
import { Input } from "@craft-bits/core";
const [name, setName] = useState("");
<Input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Ada Lovelace"
/>Uncontrolled — let the input own the value, seed it via defaultValue:
<Input defaultValue="seeded" placeholder="Username" />Compose with Field so the label, helper text, and inline error wire automatically — Field.Control injects id, aria-describedby, aria-invalid, and disabled onto the input:
import { Field, Input } from "@craft-bits/core";
<Field.Root invalid={Boolean(emailError)}>
<Field.Label>Email</Field.Label>
<Field.Control>
<Input type="email" placeholder="[email protected]" />
</Field.Control>
<Field.Error>{emailError}</Field.Error>
</Field.Root>Understanding the component
- One primitive, every native type.
typeis forwarded verbatim, so the same component renders text, email, password, search, number, tel, url, date, time, datetime-local, month, week, color, and file inputs. The file-input slot inherits the same height/typography scale as the surrounding chrome — no second component needed fortype="file". - CVA variant + size matrix. Three surfaces (
outlinefor forms,filledfor dense inline rows,ghostfor table cells or containers that already carry a border) × three heights (sm/md/lg, matchingButton's scale) cover the canonical single-line input. Variants are type-safe viaclass-variance-authority, so a typo at the call site is a compile error. - Focus + invalid states are token-driven. Focus-visible flips the border to
--cb-accentand adds a two-ring halo in--cb-accent-muted.aria-invalid="true"flips the same edges to--cb-errorwith a softer error halo — so a parentField.Roottogglinginvalidrepaints the input without any prop drilling. - Native size dropped from the prop surface. The HTML
sizeattribute (a number for character-width hints) isOmitted so the CVAsizevariant wins at the type level. Reach for the ref if you genuinely need the native attribute. - Token-only styling. Border (
--cb-border), surface (--cb-bg-elevated/--cb-bg-muted), placeholder (--cb-fg-subtle), text (--cb-fg), focus halo (--cb-accent/--cb-accent-muted), and invalid edge (--cb-error) all read from the--cb-*token system. Theme swaps and dark mode repaint without a prop change.
Props
Input extends <input> HTML attributes (with the native size attribute dropped in favour of the CVA size variant). Additional props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "outline" | "filled" | "ghost" | "outline" | Visual surface — outline for forms, filled for dense rows, ghost for chrome-inherited containers. |
size | "sm" | "md" | "lg" | "md" | Height tier. Maps to the canonical sm / md / lg scale used by Button. |
type | string | "text" | Forwarded to the native <input type> — supports every native input mode. |
className | string | — | Merged onto the underlying <input> via cn() so user classes win on conflict. |
Every other native <input> attribute (value, defaultValue, onChange, placeholder, name, disabled, readOnly, required, pattern, inputMode, autoComplete, min, max, step, multiple, accept, …) is spread onto the input.
Accessibility
- The component renders a native
<input>, so screen readers announce the role and value for free. - Focus is visible via a token-driven two-ring halo (
--cb-accentborder +--cb-accent-mutedring); the:focus-visibleselector keeps the halo off pointer focus to avoid the mouse-click highlight. aria-invalid="true"flips the border to--cb-errorand dims the focus halo to a softer error-tone — pair withField.Root invalidfor the canonical error path, or set the attribute directly when wiring withoutField.disabledremoves pointer + keyboard reachability (native<input disabled>semantics) and fades the surface to 50% opacity for visual contrast.- The file-input slot (
type="file") inherits the same height and typography scale, so the trigger button does not break the form rhythm. - Pair with
Field.Label(or any<label htmlFor>) for an accessible name — the bareInputhas no implicit label, by design.
Credits
- Extracted from:
algoflashcards(src/platform/ui/input.tsx). The source was a single-variant<input>wrapper hard-coded toh-9 rounded-3xl bg-input/50with a long Tailwind class block hand-tuned for a single visual surface. craft-bits broadens it into a CVA variant × size matrix on--cb-*tokens, wraps it inforwardRef, drops the nativesizeattribute from the prop surface, and standardises the focus + invalid state on the same--cb-accent/--cb-errorcascade the rest of the form primitives consume — soInputslots intoField.Rootwithout any glue.