Density is set by a data-density ancestor — the Context system, not a Input prop.
Installation
npx primitiv add input
pnpm dlx primitiv add input
yarn dlx primitiv add input
bunx 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
Generated from source — headless rows from each *Props type’s JSDoc, styled rows from contract.json. Never hand-maintained.
Input
Extends HTMLInputElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
asChild
boolean
false
headless
Renders 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.
children
ReactNode
—
headless
Under asChild, becomes the single child element Slot merges props
onto. Not rendered by the native <input> path (inputs are void
elements in HTML).
ref
Ref<HTMLInputElement>
—
headless
Allows 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"
md
styled
Control size; data-density scales each size further.
Styling contract
14 CSS custom properties on .primitiv-input — mode-agnostic. These names are the stable surface; the values are not.
Emitted automatically by the headless primitive — style against these rather than adding your own state classes. Grouped by the part that emits them, since most are emitted by more than one.
Input
className:.primitiv-input
Attribute
Value
When
data-disabled
""
disabled
Input
Attribute
Value
When
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
Every example below reacts to the density control. Currently showing Styled mode.
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.
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><Inputtype="email"placeholder="you@example.com"/><FieldDescription>We only use this to send receipts.</FieldDescription></Field>
import{Field}from"@primitiv-ui/react";import{Input}from"@primitiv-ui/react";<Field><Field.Label>Email address</Field.Label><Inputtype="email"placeholder="you@example.com"/><Field.Description>We only use this to send receipts.</Field.Description></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.
import{Input}from"@/components/ui/input";// The browser validates this one on submit.<Inputtype="email"requiredplaceholder="you@example.com"/>// This one is being told it is wrong by your own code.<Inputtype="email"aria-invaliddefaultValue="not-an-email"/>
import{Input}from"@primitiv-ui/react";// The browser validates this one on submit.<Inputtype="email"requiredplaceholder="you@example.com"/>// This one is being told it is wrong by your own code.<Inputtype="email"aria-invaliddefaultValue="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.
import{Input}from"@/components/ui/input";<InputdisableddefaultValue="Read only for now"/>
import{Input}from"@primitiv-ui/react";<InputdisableddefaultValue="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.