Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Input Group

Attach addons, buttons, and helper text to inputs and textareas.

Playground

—children
—InputGroupAddon align
—children
—disabled
—aria-invalid
Usage
InputGroup wraps an InputGroupInput (or InputGroupTextarea) plus any number of InputGroupAddon parts. The group owns the border, focus ring, and invalid styles — the control inside renders borderless. Place addons after the control in the DOM and position them with align, so focus lands on the field first.
12 results
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
} from "@workspace/ui/components/input-group"

<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon>
    <MagnifyingGlassIcon />
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <InputGroupText>12 results</InputGroupText>
  </InputGroupAddon>
</InputGroup>
Alignment
Four align values: inline-start and inline-end sit inside the row, while block-start and block-end switch the group to a column and span the full width — the group grows to h-auto automatically.
Full Name
USD
<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon align="inline-start">
    <MagnifyingGlassIcon />
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="Searching..." />
  <InputGroupAddon align="inline-end">
    <Spinner />
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput id="name" placeholder="Enter your name" />
  <InputGroupAddon align="block-start">
    <InputGroupText>Full Name</InputGroupText>
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="Enter amount" />
  <InputGroupAddon align="block-end">
    <InputGroupText>USD</InputGroupText>
  </InputGroupAddon>
</InputGroup>
Icon
Drop a Phosphor icon straight into the addon — unsized SVGs snap to size-4 and inherit text-muted-foreground. A group accepts addons on both edges, and one addon can hold several icons.
<InputGroup>
  <InputGroupInput type="email" placeholder="Enter your email" />
  <InputGroupAddon>
    <EnvelopeIcon />
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="Card number" />
  <InputGroupAddon>
    <CreditCardIcon />
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <CheckIcon />
  </InputGroupAddon>
</InputGroup>
Text
InputGroupText renders muted inline text for prefixes, suffixes, and counters. Pair a start and an end addon for currency or URL patterns — the addon click forwards focus into the input, so the whole affordance feels like one field.
$
USD
https://
@company.com
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>$</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="0.00" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>USD</InputGroupText>
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupAddon>
    <InputGroupText>https://</InputGroupText>
  </InputGroupAddon>
  <InputGroupInput placeholder="example.com" />
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="Enter your username" />
  <InputGroupAddon align="inline-end">
    <InputGroupText>@company.com</InputGroupText>
  </InputGroupAddon>
</InputGroup>
Button
InputGroupButton wraps Button with compact sizes tuned for the 9-unit row: xs (default), icon-xs, sm, and icon-sm. The variant defaults to ghost; icon-only buttons need an aria-label.
<InputGroup>
  <InputGroupInput placeholder="https://x.com/westy" readOnly />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Copy">
      <CopyIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
<InputGroup>
  <InputGroupInput placeholder="Type to search..." />
  <InputGroupAddon align="inline-end">
    <InputGroupButton variant="secondary">Search</InputGroupButton>
  </InputGroupAddon>
</InputGroup>
Kbd
A Kbd inside the addon gets its radius trimmed to nest inside the group corner — the classic command-palette search affordance.
⌘K
<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon>
    <MagnifyingGlassIcon />
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <Kbd>⌘K</Kbd>
  </InputGroupAddon>
</InputGroup>
Spinner
Combine Spinner with InputGroupText for loading states — the spinner inherits the addon's muted color and 16px icon sizing.
Saving...
<InputGroup>
  <InputGroupInput placeholder="Saving changes..." />
  <InputGroupAddon align="inline-end">
    <InputGroupText>Saving...</InputGroupText>
    <Spinner />
  </InputGroupAddon>
</InputGroup>
Textarea
InputGroupTextarea plus block addons builds editor-like frames: a block-start header and a block-end footer. Add border-b / border-t to the addons to separate them from the text area, and ml-auto to push actions to the end.
script.js
Line 1, Column 1
<InputGroup>
  <InputGroupTextarea
    placeholder="console.log('Hello, world!');"
    className="min-h-[140px] font-mono text-sm"
  />
  <InputGroupAddon align="block-start" className="border-b">
    <InputGroupText className="font-mono font-medium">
      <FileJsIcon />
      script.js
    </InputGroupText>
    <InputGroupButton size="icon-xs" className="ml-auto" aria-label="Copy">
      <CopyIcon />
    </InputGroupButton>
  </InputGroupAddon>
  <InputGroupAddon align="block-end" className="border-t">
    <InputGroupText>Line 1, Column 1</InputGroupText>
    <InputGroupButton size="sm" variant="default" className="ml-auto">
      Run <ArrowElbowDownLeftIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
Dropdown
An InputGroupButton works as a DropdownMenuTrigger via asChild — contextual actions without leaving the field.
<InputGroup>
  <InputGroupInput placeholder="Enter file name" />
  <InputGroupAddon align="inline-end">
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <InputGroupButton size="icon-xs" aria-label="More">
          <DotsThreeIcon />
        </InputGroupButton>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuGroup>
          <DropdownMenuItem>Settings</DropdownMenuItem>
          <DropdownMenuItem>Copy path</DropdownMenuItem>
          <DropdownMenuItem>Open location</DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  </InputGroupAddon>
</InputGroup>
RTL
The group uses logical order and padding utilities, so dir="rtl" mirrors start and end addons without extra classes.
١٢ نتيجة
<InputGroup dir="rtl">
  <InputGroupInput placeholder="بحث..." />
  <InputGroupAddon>
    <MagnifyingGlassIcon />
  </InputGroupAddon>
  <InputGroupAddon align="inline-end">
    <InputGroupText>١٢ نتيجة</InputGroupText>
  </InputGroupAddon>
</InputGroup>
Accessibility
InputGroup and each addon render role="group", and clicking an addon forwards focus into the input. Keep addons after the control in the DOM — align handles the visual order — so the tab sequence reaches the field before any buttons. Icon-only InputGroupButtons need an aria-label, and validation states are announced by setting aria-invalid on the control — the group border and ring restyle automatically.
<InputGroup>
  <InputGroupInput aria-invalid={hasError} placeholder="Email" />
  <InputGroupAddon align="inline-end">
    <InputGroupButton size="icon-xs" aria-label="Clear field">
      <XIcon />
    </InputGroupButton>
  </InputGroupAddon>
</InputGroup>
API Reference
Six parts compose the family: InputGroup as the bordered container, InputGroupInput and InputGroupTextarea as pre-styled controls, InputGroupAddon for positioned content, and InputGroupButton + InputGroupText for what goes inside addons. All accept their native element props. Custom controls opt in to the group's focus handling with data-slot="input-group-control".
ComponentPropDefault
InputGroupAddonalign?: "inline-start" | "inline-end" | "block-start" | "block-end""inline-start"
InputGroupButtonsize?: "xs" | "icon-xs" | "sm" | "icon-sm""xs"
InputGroupButtonvariant?: React.ComponentProps<typeof Button>["variant"]"ghost"
InputGroupInput...props: React.ComponentProps<"input">—
InputGroupTextarea...props: React.ComponentProps<"textarea">—
<InputGroup data-slot="input-group" role="group" />
<InputGroupInput data-slot="input-group-control" />
<InputGroupTextarea data-slot="input-group-control" />
<InputGroupAddon data-slot="input-group-addon"
  data-align="inline-start|inline-end|block-start|block-end" />
<InputGroupButton data-size="xs|icon-xs|sm|icon-sm" />
<InputGroupText />

Tokens used

9 design tokens consumed by the Input Group component.

  • --input

    Input

    border-inputdark:bg-input/30
  • --ring

    Ring

    has-[[data-slot=input-group-control]:focus-visible]:border-ringhas-[[data-slot=input-group-control]:focus-visible]:ring-ring/50
  • --destructive

    Destructive

    has-[[data-slot][aria-invalid=true]]:border-destructivehas-[[data-slot][aria-invalid=true]]:ring-destructive/20
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --text-sm

    Text Small (14px)

    text-sm
  • --font-weight-medium

    Font weight Medium (500)

    font-medium
  • --radius-md

    Radius 0.8× (8px)

    rounded-md
  • --radius

    Radius base (10px)

    rounded-[calc(var(--radius)-5px)]
  • --spacing

    Spacing unit (4px)

    h-9h-6size-6size-8gap-2ps-3pe-3py-1.5