Input
A styled Base UI input — all input types, Field state data attributes, error and disabled states, focus border recolor, and optional prefix/suffix addons.
- Status
- Since
0.1.0- Accessibility pattern
- native input
Last updated
Install
Add Input from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/inputUsage
import { Input } from "@/components/ui/input";
<Input type="email" placeholder="you@vegastack.com" />;Input is built on Base UI Input, so standard input attributes work alongside
Base UI render, onValueChange, Field state data attributes, and
state-function className. Its ref forwards to the underlying input element.
Examples
States
The field reflects disabled, readOnly, and aria-invalid. The invalid state
re-colors the border with the destructive-border token
(aria-invalid:border-destructive-border/(--alpha-tint-border)) — pair it with a visible error
message for non-color-dependent feedback. While the field is focused the focus tint wins the
border and the invalid tint returns on blur: a text field has no outline, so the border is its only
focus cue and nothing else may hold it.
Input types
Input forwards any HTML type to the underlying <input> — number, tel,
url, email, password, and the rest all work alongside the shared field
styling. type defaults to 'text'.
Don't use type="date", "time", or "datetime-local". They summon the
browser's native picker, which ignores every design token — its own surface,
radius, type ramp and focus ring, rendered differently on each browser and OS.
Reach for DatePicker instead: it's built
from Popover + Calendar, themed like the rest of the system, and displays
dates in the readable Jun 24, 2026 format (any Intl.DateTimeFormat options
via formatOptions).
Addons
Pass a prefix and/or suffix to render non-editable content inside the field —
a domain like app.vegastack.com/, a unit, or an icon. In addon mode the
<input> is wrapped in a bordered group, and focus/invalid/disabled styling
moves to the wrapper so the whole field reacts as one unit. Use className for
the Base UI input and containerClassName for the addon wrapper.
In addon mode the wrapper reads the inner input's state through has-*
selectors, so disabled and aria-invalid recolor the whole group rather than
just the <input> — has-disabled:* dims the group and
has-aria-invalid:border-destructive-border/(--alpha-tint-border) marks it invalid. As on a bare
field, the group's focus tint outranks the invalid one while anything inside it holds focus.
Controlled value (onValueChange)
Base UI exposes onValueChange, which receives the next string value directly
(no event unwrapping) — convenient for controlled fields and derived UI. It fires
alongside the native onChange.
const [value, setValue] = useState("");
<Input
value={value}
onValueChange={setValue}
placeholder="Type a workspace name"
/>;Slug preview appears here
Polymorphism (render)
Use Base UI's render prop to swap the underlying element while keeping the
field styling and behavior — pass an element to merge props into, or a function
for full control.
<Input render={<input data-testid="rendered-input" />} type="email" />Playground
Try every type, size, and state — flipping Invalid on replays the built-in shake — then copy the generated JSX.
<Input placeholder="you@vegastack.com" />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | ((state: InputState) => string | undefined) | — | Classes for the Base UI input element. Accepts Base UI's state-function
form, so styles can respond to field state such as focused or invalid. |
containerClassName | string | — | Classes for the wrapper used only when prefix or suffix is present. |
prefix | React.ReactNode | — | Content rendered as a non-editable addon before the input (e.g.
"app.vegastack.com/" or an icon). Switches the component into addon mode:
the <input> is wrapped in a bordered group and the border/ring/disabled
styling moves to the wrapper. Plain strings render as muted, non-selectable
label text. |
size | "lg" | "md" | "sm" | 'md' | Control height on the shared 28/32/40 scale (--size-sm/md/lg), matching
Button and Select. (The native numeric size attribute is intentionally
replaced by this variant prop.) |
suffix | React.ReactNode | — | Content rendered as a non-editable addon after the input (e.g. a unit like
".com" or an icon). Switches the component into addon mode (see prefix). |
Data attributes and CSS variables on Input
| Attribute | Values |
|---|---|
data-field-group | "" |
data-size | mirrors a prop or state value |
data-slot | "input" | "input-group" | "input-prefix" | "input-suffix" |
Accessibility
- Renders Base UI Input as a native
<input>by default — always associate a visible<label>(wrap it or usehtmlFor/id); usearia-labelonly when a visible label is impossible. - On focus the field recolors its border with the
ringtoken (focus:border-ring/(--alpha-tint-border)) instead of removing the outline — neveroutline: nonewith no replacement affordance.TextareaandOTPInputuse the identical border tint, so text-entry focus reads the same across all three. - Set
aria-invalid(andaria-describedbypointing at the error text) to mark a field invalid; the styling is token-driven and never relies on color alone. prefix/suffixaddons are decorative label text; keep the meaningful value in the input itself so assistive tech reads the field, not the chrome.
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, dragging, error, invalid |
| Accessibility | invalid, semantic-html |
| Visual | default, focus, disabled, invalid, error, dark |