Input Group
One field surface shared by a text control and its addons — prefix/suffix
text, icons, keyboard hints, or a trailing button. The group carries the same
hairline ring, sizes, and focus / invalid / disabled treatment as Input, so
grouped and plain fields align in the same form, and clicking anywhere on the
surface focuses the control. For a leading icon alone, plain Input with
icon is enough — reach for the group when content must live inside the
field.
Import
import { InputGroup } from "@photon-ai/pho-ui/components/input-group";
Anatomy
Root is the bordered surface; everything else rides on it. Input is the
control, Addon holds inline content on either side, and Separator draws a
full-height hairline between segments.
<InputGroup.Root>
<InputGroup.Addon>https://</InputGroup.Addon>
<InputGroup.Input placeholder="example.com" />
<InputGroup.Separator />
<InputGroup.Addon>
<Kbd>↵</Kbd>
</InputGroup.Addon>
</InputGroup.Root>
Addons
Addon renders in the secondary tone so the value stays the loudest thing on
the surface. It takes anything inline: plain text, a Tabler icon, Kbd caps,
or a small ghost button for a trailing action.
{
/* Search with a shortcut hint */
}
<InputGroup.Root>
<InputGroup.Addon>
<IconSearch />
</InputGroup.Addon>
<InputGroup.Input placeholder="Search projects…" aria-label="Search" />
<InputGroup.Addon>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</InputGroup.Addon>
</InputGroup.Root>;
{
/* Read-only value with a copy action */
}
<InputGroup.Root>
<InputGroup.Input
readOnly
defaultValue="pho_sk_51Kk…9fD3"
aria-label="API key"
/>
<InputGroup.Addon>
<Button variant="ghost" size="sm" svgOnly aria-label="Copy API key">
<IconCopy />
</Button>
</InputGroup.Addon>
</InputGroup.Root>;
Segments
Separator turns an addon into a boxed section — the classic
https:// │ workspace │ .photon.dev arrangement. It’s a Base UI Separator
(orientation="vertical") drawn with the site hairline, inset 2px from the
top and bottom so it never overlaps the ring — resting or focused.
<InputGroup.Root>
<InputGroup.Addon>https://</InputGroup.Addon>
<InputGroup.Separator />
<InputGroup.Input placeholder="workspace" aria-label="Subdomain" />
<InputGroup.Separator />
<InputGroup.Addon>.photon.dev</InputGroup.Addon>
</InputGroup.Root>
Sizes
The same three sizes as Input — sm, md, lg (default md) — landing
the same 36 / 40 / 44px heights, so grouped and plain fields line up row for
row. lg gets the same squircle corner smoothing as the lg Input. Addon
icons scale with the size (16px at sm, 20px at md / lg).
<InputGroup.Root size="sm">…</InputGroup.Root>
<InputGroup.Root size="md">…</InputGroup.Root>
<InputGroup.Root size="lg">…</InputGroup.Root>
With a label
InputGroup.Input renders Base UI Field.Control, so inside a Field.Root
the label and description wire up automatically — no htmlFor bookkeeping.
Standalone, give the control an aria-label instead.
The domain your project deploys to.
<Field.Root>
<Field.Label>Site URL</Field.Label>
<InputGroup.Root>
<InputGroup.Addon>https://</InputGroup.Addon>
<InputGroup.Input placeholder="example.com" />
</InputGroup.Root>
<Field.Description>The domain your project deploys to.</Field.Description>
</Field.Root>
Invalid & disabled
invalid on Root renders the error ring and sets aria-invalid on the
control. Disabling the control (disabled on InputGroup.Input) dims the
whole surface and blocks the click-to-focus behavior.
<InputGroup.Root invalid>
<InputGroup.Addon>
<IconCreditCard />
</InputGroup.Addon>
<InputGroup.Input defaultValue="4242 4242 4242" aria-label="Card number" />
</InputGroup.Root>
<InputGroup.Root>
<InputGroup.Addon>https://</InputGroup.Addon>
<InputGroup.Input placeholder="Unavailable" disabled />
</InputGroup.Root>
Props
InputGroup.Root
- size —
sm·md·lg(defaultmd) — theInputsize scale - invalid —
boolean— error ring on the surface,aria-invalidon the control - …native
<div>attributes
InputGroup.Input
- …native
<input>attributes —value,onChange,placeholder,disabled,aria-*, … (everything exceptsize, which lives onRoot)
InputGroup.Addon
- …native
<div>attributes — content is anything inline: text, icons,Kbd, a small button
InputGroup.Separator
- …Base UI
Separatorprops —orientationdefaults tovertical
Accessibility
- Labelling — the control is Base UI
Field.Control: inside aField.Rootit inherits the label/description wiring; standalone, passaria-label. Addon text is visual context, not a label — screen readers don’t associatehttps://with the field on their own. - Click-to-focus — pointer-down anywhere on the surface focuses the control, except on interactive children (buttons, links), which keep their own behavior and don’t steal focus from the field.
- Invalid —
invalidstyles the surface and setsaria-invalidon the control, so the state is announced, not just painted. - Addon buttons — an icon-only action inside an addon needs
svgOnlyand anaria-label, same as any icon button.
Best practices
- Use an addon for content that’s part of the value’s context — a protocol prefix, a unit, a shortcut hint. Put field-level actions like “Forgot?” in the label row, not inside the field.
- Keep one control per group. Two inputs on one surface read as one field to
the eye but two stops to the keyboard — use
Fieldsetfor that. - Prefer plain
Inputwhen all you need is a leading icon; the group earns its extra markup only when addons carry real content. - Trailing buttons should be quiet (
ghost,sm) — the field is the subject, the action is a convenience.