Pho Design System

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.

K
https://
https://

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.

https://
.photon.dev
<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 Inputsm, 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).

K
K
K
<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.

https://

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

InputGroup.Input

InputGroup.Addon

InputGroup.Separator

Accessibility

Best practices