Photon Design System

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 Photon’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.

SizePadding (effective)Text (size / line-height)HeightRadius
sm12 / 8pxtext-base — 14 / 20px36pxrounded-base (12px)
md12 / 8pxtext-md — 16 / 24px40pxrounded-base (12px)
lg14 / 10pxtext-md — 16 / 24px44pxsquircle 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="you@photon.dev"
  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.

Forgot?
<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 when invalid
  • size — sm · md · lg (default md) — 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 native required attribute 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 except size, which is the size variant above)

Accessibility

Built on Base UI Field, which associates the parts for you:

  • Labelling — label is tied to the <input> (Field.Label ↔ Field.Control), so clicking it focuses the field and assistive tech announces the name. Always pass a label; when a visible one is truly impossible, forward aria-label through the native attributes instead.
  • Helper text — hint renders as Field.Description and is wired to the input via aria-describedby, so it’s read out with the field.
  • Invalid — invalid is a visual state only (ring, icon, hint color). To announce invalidity programmatically, forward aria-invalid and keep the reason in hint.
  • Required — required sets the native required attribute; 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) so Tab moves field-to-field; it’s operable by pointer and carries aria-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; reserve aria-label for 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 — set invalid, forward aria-invalid, and say what’s wrong.
  • Choose type deliberately (email, password, url): it drives the right mobile keyboard, and password earns the reveal toggle for free.
  • Keep the leading icon decorative — 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.