InputGroup
A hand-drawn field that composes a bare input with leading and trailing addons in one sketchy box.
Overview
InputGroup composes a bare text input with one or more leading and/or trailing addons — icons,
short text, or buttons — inside a single hand-drawn box. Use it for fields that need an affordance
attached to the input: a https:// prefix, a currency unit, a search icon, or a submit button.
Anatomy
A flex row painting one sketchy SketchSurface box around its children:
InputGroupInput— a bare, borderless, transparent<input>that grows to fill the row.InputGroupAddon— a leading or trailing slot (icon, text, or button), rendered in muted text color. Its position is set by child order, before or after the input.
Focus anywhere inside the group switches the box outline to the focus stroke. Every color, padding,
radius, and sketch parameter comes from @ghds/tokens (comp.inputGroup.*).
Variants & States
There are no named variants — instead the group carries disabled and invalid booleans, and it
reacts to focus.
| State | Status | Notes |
|---|---|---|
| Focus | Implemented | Focus within the group switches the outline to comp.inputGroup.stroke.focus. |
| Invalid | Implemented | The invalid prop paints the danger stroke. |
| Disabled | Implemented | Disables the input and mutes the fill. |
| Hover | Not implemented | — |
| Loading | Not implemented | — |
| Error | Implemented | Same as invalid — there is no separate error state. |
Usage
Do
- Place the addon before or after InputGroupInput in child order to make it lead or trail.
- Set invalid together with an external error message so the reason is explained, not just colored.
- Give InputGroupInput an aria-label (or an associated <label>) — the group is not a labeled field by itself.
Don't
- Don't rely on the danger stroke alone to convey an error — pair it with text describing the problem.
- Don't put more than one InputGroupInput in a group; it is one field with addons, not a multi-input row.
Accessibility
- The group aggregates focus/blur from its inner controls to paint one shared focus outline; it is not itself an interactive target and takes no role.
- Label the field via
InputGroupInput(anaria-labelor an associated<label>) — the addon is decorative-ish text, not a substitute for a label. - Setting
disabledon the group disablesInputGroupInputthrough context; an individual input’s owndisabledprop still wins if you set it. - Pair
invalidwitharia-invalid/ an error message so the state is announced, not only painted. - See the Accessibility Guide for shared conventions.
Content
Keep addons short — a prefix (https://, @), a unit (USD, / mo), an icon, or a single small
button. Long addon text competes with the input value for space.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disables the whole group and mutes the surface (InputGroup). |
invalid | boolean | false | Marks the group invalid, painting the danger stroke (InputGroup). |
...rest | HTMLAttributes<HTMLDivElement> | — | Native <div> attributes pass through to the group container. |
InputGroupInput | InputHTMLAttributes (minus 'size') | — | The bare inner <input>; accepts native input attributes. |
InputGroupAddon | HTMLAttributes<HTMLSpanElement> | — | A leading/trailing addon <span>; position set by child order. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Reflected attribute; disables the group and mutes the surface. |
invalid | boolean | false | Reflected attribute; paints the danger stroke. |
slot | (default) | — | Slot addons (text/icons) and a bare <input> in child order; a slotted <input> loses its own chrome and the group paints the box. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disables the whole group and mutes the surface (InputGroup). |
invalid | boolean | false | Marks the group invalid, painting the danger stroke (InputGroup). |
InputGroupInput | TextInputProps (minus editable/onFocus/onBlur) + disabled | — | The bare inner TextInput; disabled is inherited from the group unless set. |
InputGroupAddon | { children } | — | A leading/trailing addon; a string child renders as muted addon text. |
testID | string | — | Test handle for queries (InputGroup). |
Code examples
// React — @ghds/react (addon position set by child order)
import { InputGroup, InputGroupAddon, InputGroupInput } from '@ghds/react/input-group';
<InputGroup>
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput placeholder="example.com" aria-label="Website" />
</InputGroup>;
<!-- Web Components — @ghds/web-components (slot a bare <input>; the group paints the box) -->
<script type="module">
import '@ghds/web-components/input-group';
</script>
<gh-input-group>
<span>https://</span>
<input aria-label="Website" placeholder="example.com" />
</gh-input-group>
// React Native — @ghds/react-native
import { InputGroup, InputGroupInput, InputGroupAddon } from '@ghds/react-native/input-group';
<InputGroup>
<InputGroupAddon>https://</InputGroupAddon>
<InputGroupInput placeholder="example.com" accessibilityLabel="Website" />
</InputGroup>;
Live Demo
React and Web Components render live below, driven by the same tokens — toggle dark mode in the
header to see both respond identically. React Native can’t run in a browser without additional
react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.