Input
A single-line text field built on Base UI Field, so the label, helper text,
and control are wired together for accessibility. It adds Pho’s bordered
surface, a leading icon slot, an invalid state, and a built-in password reveal
toggle. For multi-field layouts and validation, compose it inside a Form.
We'll never share it.
That username is already taken.
Import
import { Input } from "@photon-ai/pho-ui/components/input";
Sizes
Three sizes — sm, md, lg — scaling padding and text. The default is md.
Height is the line-height plus vertical padding (the ring is drawn inset, so it
adds no height), which lands lg at 44px — matching the lg Button and the
auth pill. Corner radius is rounded-base (12px) at every size; lg also gets
squircle smoothing.
| Size | Padding (effective) | Text (size / line-height) | Height | Radius |
|---|---|---|---|---|
sm |
12 / 8px | text-base — 14 / 20px |
36px | rounded-base (12px) |
md |
12 / 8px | text-md — 16 / 24px |
40px | rounded-base (12px) |
lg |
14 / 10px | text-md — 16 / 24px |
44px | squircle rounded-base (12px) |
Corners nest concentrically (inner radius = outer radius − inset): the wrapper
insets the control 2px so its autofill fill rounds at outer − 2px inside the
ring, and at lg the hairline’s inner edge is held at 11px (rest) / 10px
(focused) rather than the browser’s superellipse inset-shadow derivation, which
otherwise thickens at the corners. The effective padding above already includes
the 2px inset.
<Input size="sm" label="Small" />
<Input size="md" label="Medium" />
<Input size="lg" label="Large" />
Leading icon
icon takes an icon component (e.g. a Tabler icon) and renders it inside the
left edge of the field, with the control padded to clear it. The icon is
decorative — the label still carries the meaning.
import { IconMail } from "@tabler/icons-react";
<Input
label="Email"
type="email"
placeholder="[email protected]"
icon={IconMail}
/>;
Password
type="password" adds a reveal toggle at the right edge that swaps the field
between hidden and plain text. No extra wiring — pass the type and the toggle
appears.
<Input label="Password" type="password" placeholder="••••••••" />
Invalid
invalid renders the error treatment: an error-colored ring, a warning icon,
and the hint recolored to the error tone. Put the reason in hint so the
field explains itself.
<Input
label="Username"
defaultValue="ada"
invalid
hint="That username is already taken."
/>
Disabled
disabled dims the field surface and blocks input; it flows through Base UI
Field, though only the bordered surface dims — the label and hint keep their
normal styling.
<Input label="Workspace" placeholder="Unavailable" disabled />
Label actions
labelTrailing places a node at the right edge of the label row — a secondary
action like a “Forgot?” link — without crowding the field itself. Mark a
required field with required, which adds a brand asterisk and the native
required attribute.
<Input
label="Password"
type="password"
labelTrailing={
<a href="/reset" className="text-pho-brand text-base font-medium">
Forgot?
</a>
}
/>
Props
- label —
ReactNode— the field label, associated with the control for accessibility - hint —
ReactNode— helper text under the field; recolored to the error tone wheninvalid - size —
sm·md·lg(defaultmd) — padding and text scale - invalid —
boolean— the visual invalid state (error ring + warning icon + error-colored hint) - icon —
ComponentType<{ className?: string }>— leading icon component - labelTrailing —
ReactNode— node at the right edge of the label row (e.g. a “Forgot?” link) - required —
boolean— sets the nativerequiredattribute and shows a brand asterisk - disabled —
boolean— native disabled state; dims the field surface and blocks input - wrapperClassName —
string— class applied to the bordered field surface - className —
string— class applied to the<input>element - type —
string(default"text") — native input type;"password"adds the reveal toggle - …native
<input>attributes —value,onChange,name,placeholder,aria-*, … (everything exceptsize, which is the size variant above)
Accessibility
Built on Base UI Field, which associates the parts for you:
- Labelling —
labelis tied to the<input>(Field.Label↔Field.Control), so clicking it focuses the field and assistive tech announces the name. Always pass alabel; when a visible one is truly impossible, forwardaria-labelthrough the native attributes instead. - Helper text —
hintrenders asField.Descriptionand is wired to the input viaaria-describedby, so it’s read out with the field. - Invalid —
invalidis a visual state only (ring, icon, hint color). To announce invalidity programmatically, forwardaria-invalidand keep the reason inhint. - Required —
requiredsets the nativerequiredattribute; the asterisk is decorative.
Keyboard:
- Type / edit — standard single-line text-field editing.
- Password toggle — the reveal button is deliberately outside the tab order (
tabIndex=-1) soTabmoves field-to-field; it’s operable by pointer and carriesaria-label="Toggle password visibility".
Focus shows a 2px brand ring on :focus-visible (solid error when invalid).
The resting invalid edge is ring-pho-error-subtle at 1.5px — between the
1px hairline and the 2px focus ring — so the error reads without competing
with focus.
Best practices
- Always pair an input with a
label; reservearia-labelfor the rare field that genuinely can’t show one, like a search box beside an icon. - Put helper and error copy in
hint, and never signal an error with color alone — setinvalid, forwardaria-invalid, and say what’s wrong. - Choose
typedeliberately (email,password,url): it drives the right mobile keyboard, andpasswordearns the reveal toggle for free. - Keep the leading
icondecorative — it sharpens recognition but must never be the only cue for what the field is. - Put a secondary action like “Forgot?” in
labelTrailing, not floating inside the field where it competes with the value.