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.

Preview
Variants
Sizes
Types
States

Installation

npx shadcn@latest add https://craftbits.dev/r/input.json

Usage

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

  1. One primitive, every native type. type is 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 for type="file".
  2. CVA variant + size matrix. Three surfaces (outline for forms, filled for dense inline rows, ghost for table cells or containers that already carry a border) × three heights (sm / md / lg, matching Button's scale) cover the canonical single-line input. Variants are type-safe via class-variance-authority, so a typo at the call site is a compile error.
  3. Focus + invalid states are token-driven. Focus-visible flips the border to --cb-accent and adds a two-ring halo in --cb-accent-muted. aria-invalid="true" flips the same edges to --cb-error with a softer error halo — so a parent Field.Root toggling invalid repaints the input without any prop drilling.
  4. Native size dropped from the prop surface. The HTML size attribute (a number for character-width hints) is Omitted so the CVA size variant wins at the type level. Reach for the ref if you genuinely need the native attribute.
  5. 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:

PropTypeDefaultDescription
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.
typestring"text"Forwarded to the native <input type> — supports every native input mode.
classNamestringMerged 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-accent border + --cb-accent-muted ring); the :focus-visible selector keeps the halo off pointer focus to avoid the mouse-click highlight.
  • aria-invalid="true" flips the border to --cb-error and dims the focus halo to a softer error-tone — pair with Field.Root invalid for the canonical error path, or set the attribute directly when wiring without Field.
  • disabled removes 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 bare Input has no implicit label, by design.

Credits

  • Extracted from: algoflashcards (src/platform/ui/input.tsx). The source was a single-variant <input> wrapper hard-coded to h-9 rounded-3xl bg-input/50 with 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 in forwardRef, drops the native size attribute from the prop surface, and standardises the focus + invalid state on the same --cb-accent / --cb-error cascade the rest of the form primitives consume — so Input slots into Field.Root without any glue.