Skip to content

Input

Stable

A single-line text field for names, emails, numbers and search — always paired with a visible label.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/input.json

Usage

import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
<Field>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" placeholder="you@company.com" />
</Field>

An input on its own has no visible name. In forms, always compose it with Field, which lays out the label, description and error message consistently.

Examples

Sizes

sm, default and lg share their heights with Button and Select — 32, 36 and 40px — so an input and its action always align on one row.

Types

prfct styles every native input type. Choose the right type and inputMode so mobile keyboards match the data, and set autoComplete so browsers and password managers can fill the field.

Invalid

Set aria-invalid on the input and data-invalid on its Field, and explain the problem with a FieldError linked through aria-describedby. The border turns red and the focus halo follows, but the message and its icon carry the meaning — never color alone.

Disabled and read-only

Use disabled when a value can't be changed right now; it is skipped by keyboard focus and left out of form submissions. Use readOnly when people need to see, select or copy a value they can't edit — it stays focusable and is submitted with the form.

File

File inputs get a quiet, bordered selector button that matches the field. For drag-and-drop or multiple files with previews, build on top of this rather than replacing the native input — it keeps keyboard and assistive technology support for free.

With a button

Because heights are shared, an input and a button sit side by side without adjustment. Give the input an aria-label when there is no visible label.

Controlled

Pass value and onChange to derive UI from what people type — here, a character count and a live URL preview. Prefer uncontrolled inputs with defaultValue when you only need the value on submit.

Guidelines

When to use

  • Short, free-form text on a single line: names, emails, URLs, amounts, search queries.
  • Values with a native type the browser already understands: email, number, date, url, password.

When not to use

  • Paragraphs or multi-line text — use a Textarea.
  • Choosing from a known list — use a Select or, for long lists, a Combobox.
  • Fixed-length codes such as 2FA — use Input OTP.
  • Icons, units or buttons inside the field — use an Input Group.

Labels, not placeholders

A placeholder disappears the moment someone starts typing, so it can't carry the field's name or instructions. Put the name in a visible label, instructions in a description, and use the placeholder — if at all — for an example of the expected format.

Do.A persistent label names the field; the placeholder only shows an example.
Don’t.A placeholder as the only label vanishes as soon as people type.

Width is a hint

The width of a field tells people how much to type. Size fields to their expected content — a postal code should look shorter than an address — instead of stretching every input to the container.

Do.Widths reflect the expected length of each value.
Don’t.Uniform full-width fields hide which answers are short.

Validate at the right moment

Validate on submit and when a field loses focus — not on every keystroke, which flags people as wrong before they have finished typing. Once a field is invalid, re-validate as they type so the error clears the moment it is fixed. See Field for the full pattern.

Accessibility

Input renders a native <input> through Base UI, so it keeps every browser behavior — autofill, spellcheck, IME composition, form submission — and works inside Base UI's Field and Form when you need built-in validation.

  • Name every input. Associate a visible label with htmlFor/id, or give inputs without a visible label an aria-label.
  • Describe and explain. Link descriptions and errors with aria-describedby; set aria-invalid while a value is invalid.
  • Identify purpose. Set autoComplete on personal data such as name, email and tel (WCAG 1.3.5).
  • Contrast. The field border uses the input token, which meets 3:1 against the page for non-text contrast (WCAG 1.4.11). Placeholder text uses muted-foreground at 4.5:1 or more.
  • No zoom on focus. Text is 16px below the md breakpoint so iOS Safari doesn't zoom the page when the field is focused, then 14px on larger screens.
KeyBehavior
Tab
Moves focus to the input. Focus shows as a brand-colored border with a soft halo.
ShiftTab
Moves focus to the previous focusable element.
Enter
Submits the enclosing form.

API reference

Input

Renders an <input> element. Accepts every native input attribute and the props of the Base UI Input. The native size attribute is replaced by prfct's size variant.

PropTypeDefault
size

Height and horizontal padding: 32, 36 or 40px, matching Button and Select.

"sm" | "default" | "lg""default"
type

Any native input type: email, password, number, date, url, file, search…

string"text"
value

The controlled value. Pair with onChange.

string | numberNo default
defaultValue

The initial value of an uncontrolled input.

string | numberNo default
disabled

Prevents interaction, removes the input from the tab order and from form submission.

booleanfalse
readOnly

Prevents editing while keeping the value focusable, selectable and submitted.

booleanfalse
aria-invalid

Marks the value as invalid: red border and red focus halo. Pair with a FieldError.

booleanNo default

inputVariants

The class generator is exported for elements that should look like an input — for example a read-only value rendered as a <div>.

import { inputVariants } from "@/components/ui/input"
import { cn } from "@/lib/utils"

<div className={cn(inputVariants({ size: "sm" }), "flex items-center font-mono")}>
  ws_8f3k29d1
</div>