Skip to content
HyperDesign System

Input

A single-line text field: 32px, the input-tint outline, a louder edge on hover and the brand ring on focus. Text is 16px on a phone so iOS never zooms, 14px from md. Source: src/components/ui/input.tsx.

Default

Always pair an input with a visible Label. A hint underneath is linked with aria-describedby so screen readers read it after the label.

Shown in the sidebar and in shared links.

Types

The native type does the work: the right keyboard on a phone, autofill, the password mask, number steppers and the file picker.

type="email"
type="password"
type="number"
type="search"
type="file"

Variants

Two. Default draws the box; bare removes it so the input can live inside a shell you draw, keeping only the text styles and placeholder tier.

variant="default"
variant="bare"

Need a prefix icon, a unit or an inline button inside the field? Use Input group rather than hand-building a bare shell.

States

Hover lifts the outline, focus adds the 3px brand ring, disabled fades to 50% on a tint fill. aria-invalid swaps both outline and ring to destructive.

placeholder
filled
readOnly
disabled
aria-invalid="true"

Read-only has no style of its own; it looks like a normal field but cannot be edited. There is no loading state.

Validation

The system has no Field or error-message primitive yet. The input styles aria-invalid; the label, message and their wiring are composed by hand, as below. Try submitting empty, then type.

Validate on blur and submit

We send run summaries here.

Character limit

13/32

With a button

An inline form: the input fills the row and a sm Button (32px, the same height) submits it. A visually hidden label is acceptable only when the placeholder and button make the purpose obvious.

Props

Input props
PropTypeDefaultDescription
variant"default" | "bare""default"bare drops height, padding, border, radius and the focus ring for a field that sits inside another shell (a menu search row, the composer). Mirrored as data-variant.
typeHTMLInputTypeAttribute—Passed to the native input. file inputs get a styled 24px file button.
aria-invalidboolean | "true" | "false"—Turns the outline destructive with a 3px destructive ring. The message is yours to render.
...propsComponentProps<"input">—Native attributes pass through (disabled, readOnly, placeholder, value / onChange). className merges last. There is no size prop: every input is 32px tall.

Best practices

When to use

  • One line of free text: names, emails, URLs, numbers. Use Textarea for anything that wraps.
  • Picking from a known list is a Select, not an input with validation.
  • Prefixes, suffixes and inline actions belong in Input group.

Behavior

  • Validate on blur or submit, never on every keystroke while the person is still typing.
  • Once a field is invalid, clear the error as soon as the value becomes valid.
  • Set type and autoComplete so phones show the right keyboard and autofill works.

Accessibility

  • Every input has a Label with a matching htmlFor, or an aria-label when the label is truly redundant.
  • Link hints and errors with aria-describedby; set aria-invalid only after the person has interacted.
  • Placeholder is an example, not a label: it disappears on input and sits on the third text tier.

Content

  • Labels are nouns in sentence case: Work email, not EMAIL ADDRESS:.
  • Errors say what is wrong and how to fix it: That doesn't look like an email. Check for an @.