Skip to content
Primitiv home
Framework
Consumption mode

Input

stableSource Figma

A single-line text input — the framed field consumers type into.

Playground

Density

Preview

Size
import { Input } from "@/components/ui/input";
<Input size="md" type="email" placeholder="you@example.com" />

Density is set by a data-density ancestor — the Context system, not a Input prop.

Installation

npx primitiv add input

Import

import { Input } from "@/components/ui/input";

Copied into your project as .primitiv-input — you own the file afterwards, so upgrades are opt-in.

Headless mode installs the npm package instead: @primitiv-ui/react

Props

Input

Extends HTMLInputElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
asChildbooleanfalseheadlessRenders the child element instead of a native <input>, merging all props — aria-*, data-*, event handlers, ref — onto it via Slot. Event handlers compose (child runs first). type is not forwarded in this mode; the child owns its own type semantics. See the asChild example on Input.
childrenReactNodeheadlessUnder asChild, becomes the single child element Slot merges props onto. Not rendered by the native <input> path (inputs are void elements in HTML).
refRef<HTMLInputElement>headlessAllows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). Forwarded to the underlying HTMLInputElement. Under asChild, merged onto the rendered child via Slot.
size"xs" | "sm" | "md" | "lg" | "xl"mdstyledControl size; data-density scales each size further.

Styling contract

--primitiv-input-bg--primitiv-input-fg--primitiv-input-placeholder-color--primitiv-input-border-color--primitiv-input-border-color-focus--primitiv-input-border-color-invalid--primitiv-input-border-width--primitiv-input-radius--primitiv-input-height--primitiv-input-padding-inline--primitiv-input-font-family--primitiv-input-font-size--primitiv-input-font-weight--primitiv-input-line-height

Data attributes

Input

className: .primitiv-input

AttributeValueWhen
data-disabled""disabled

Accessibility

  • An <input> has no implicit accessible name. Give it a <label htmlFor>, an aria-label, or an aria-labelledby — or let Field wire the label, the id and the description for you. A placeholder is not a label: it disappears on the first keystroke and is not reliably announced.
  • Constraint validation is the platform's. required, type, pattern and friends work untouched, and the browser owns :invalid, the submit block and the native message.
  • aria-invalid is for an error your own code knows about — a server response, a form library — and is what the invalid ring keys off. It is deliberately separate from :invalid, so a field is not painted red the moment it is focused and still empty.
  • disabled exposes both the native attribute and data-disabled, so the hook and the behaviour cannot drift: the platform removes it from the tab order and from form submission.
  • Under asChild every attribute, handler and the ref transfer to your element, so it keeps the input's wiring — but type is not forwarded, since the element you supply owns its own type semantics.

Examples

Labelling

An <input> has no implicit accessible name, so every one needs a label — a <label htmlFor>, an aria-label, or aria-labelledby. Field does the wiring for you: it generates the id, points the label at it, and links a description through aria-describedby, so the three stay in step when the input is replaced.

Density
We only use this to send receipts.
import { Field, FieldLabel, FieldDescription } from "@/components/ui/field";import { Input } from "@/components/ui/input";
<Field>  <FieldLabel>Email address</FieldLabel>  <Input type="email" placeholder="you@example.com" />  <FieldDescription>We only use this to send receipts.</FieldDescription></Field>

Native validation

Every HTML constraint attribute works as the browser intends — required, type, pattern, minLength — because the component does not interfere with them. The browser sets :invalid itself and blocks submission. aria-invalid is the separate, deliberate hook for showing a server or library error, and it is what the stylesheet keys the invalid ring off.

Density
import { Input } from "@/components/ui/input";
// The browser validates this one on submit.<Input type="email" required placeholder="you@example.com" />
// This one is being told it is wrong by your own code.<Input type="email" aria-invalid defaultValue="not-an-email" />

Disabled

disabled sets the native attribute and exposes data-disabled, so the styling hook and the real behaviour can never disagree — a disabled input is skipped by the tab order and omitted from form submission because the platform says so, not because CSS made it look that way.

Density
import { Input } from "@/components/ui/input";
<Input disabled defaultValue="Read only for now" />

As another element (asChild)

asChild renders your element instead of the native <input>, merging every prop, data-*, handler and the ref onto it — for a masked-input or autocomplete library that insists on owning the element. One asymmetry worth knowing: type is not forwarded in this mode, because the child owns its own type semantics.

Density
import { Input } from "@/components/ui/input";
<Input asChild>  <input type="search" placeholder="Search..." /></Input>