Input group
An input with addons inside its border: icons, text, buttons.
In Accessible first
See it live
npm run storybookWhen to use it
In Accessible first: A value with a unit or currency, with the label still above the field. Not for separate fields.
- Use it when: A search with an icon and a clear button, a value with a unit or currency.
- Not when: Controls that are separate fields (lay them out with field).
Instead:
- Input: A plain field.
Variants
What each variant is for in any design system, then what Accessible first decided and why. Each sample is the live story with that variant set.
inline-startalignUsed in Accessible firstWhat it is for
An addon before the text, on the same line: an icon or currency.
In Accessible first
A currency sign before the amount, with the label still above the field.
inline-endalignUsed in Accessible firstWhat it is for
An addon after the text: a unit, a clear button, a shortcut.
In Accessible first
A unit after the value, as a word people can read, with the label still above the field.
block-startalignNot used in Accessible firstWhat it is for
An addon row above the text, inside the field.
In Accessible first
The label stays above the field, outside it, always visible.
block-endalignNot used in Accessible firstWhat it is for
An addon row below the text, inside the field.
In Accessible first
Hints and errors sit in the field layout, in the same place on every question.
Why it works this way
Input group looks the way it does because of 4 answers about this product. Each card is one answer, in three steps: the answer, what it decided for Input group, and what would move if it changed.
people.q21The answer
AAA where it applies; nobody is left out.
2So Input group
- ✓uses inline-start: A currency sign before the amount, with the label still above the field.
- ✓uses inline-end: A unit after the value, as a word people can read, with the label still above the field.
- –does not use block-start: The label stays above the field, outside it, always visible.
- –does not use block-end: Hints and errors sit in the field layout, in the same place on every question.
Button: danger Button: focus Button: disabled Field: vertical Field: horizontal3If it changed
and 15 answers that follow from it: A ratio, line heights in proportion, Neutrals and one brand ramp, Nothing, and 12 more.
tasks.q11The answer
People must finish the form.
2So Input group
- ✓uses inline-start: A currency sign before the amount, with the label still above the field.
Field: vertical3If it changed
and 6 answers that follow from it: Completion without errors, A task that fails, One sitting, and 3 more.
iconography.roles1The answer
Icons support words; they never replace them.
2So Input group
- ✓uses inline-end: A unit after the value, as a word people can read, with the label still above the field.
Button: icon Toggle: outline3If it changed
No other answer follows from it.
voice.roles1The answer
Hints and help text carry people through each step.
2So Input group
- –does not use block-end: Hints and errors sit in the field layout, in the same place on every question.
3If it changed
No other answer follows from it.
Words
- Use common words. Explain any term you must use.
- One idea per sentence. Put the action first.
- Never blame the person.
- Use: Continue, Change, Start, You. Avoid: Invalid, Utilise, Submit, Error code.
The labels components use, from the voice tokens:
content.action.commit: Continuecontent.action.cancel: Go backcontent.action.destroy: Deletecontent.action.retry: Try againcontent.action.empty-action: Start a new application
See Voice and tone for the whole voice.
Accessibility and keyboard
Test before you ship:
- Addon buttons have names; icons that only decorate are hidden from screen readers. undefined
Use it in code
npx open-ds add input-group
The component reads only component keys and semantic tokens, so it follows the brand and mode on <html>.
Tokens it reads
Input group reads 37 tokens of its own, each an alias of a semantic token. Each row is one part: its own token, the semantic it aliases, and the value it ends at. Change a semantic and every component that reads it follows; override one of Input group's tokens to change Input group alone.
Accessible first decides what changes when you switch theme. Here a switch changes colors only, so the marked rows differ between light and dark; everything else holds steady.
| Part | Its token and the semantic it aliases | Value |
|---|---|---|
| Input group | ||
| Corner | component.input-group.radiusradius.controlvar(--input-group-radius) | radius.6:0.375rem |
| Border Changes with theme | component.input-group.bordercolor.border.strongvar(--input-group-border) | Lightgray.light.500:#595959Darkgray.dark.500:#949494 |
| Shadow | component.input-group.shadowelevation.cardvar(--input-group-shadow) | shadow.none |
| Padding left | component.input-group.padding-leftcomponent-spacing-map.x2var(--input-group-padding-left) | space.8:0.5rem |
| Padding right | component.input-group.padding-rightcomponent-spacing-map.x2var(--input-group-padding-right) | space.8:0.5rem |
| Padding, bottom | component.input-group.padding-bottomspace.inset.control-xvar(--input-group-padding-bottom) | space.24:1.5rem |
| Padding, top | component.input-group.padding-topspace.inset.control-xvar(--input-group-padding-top) | space.24:1.5rem |
| Border 2 Changes with theme | component.input-group.border-2color.border.focusvar(--input-group-border-2) | Lightblue.light.600:#0B4FC0Darkblue.dark.600:#8AB4F8 |
| Focus ring Changes with theme | component.input-group.focus-ringcolor.border.focusvar(--input-group-focus-ring) | Lightblue.light.600:#0B4FC0Darkblue.dark.600:#8AB4F8 |
| Border invalid Changes with theme when invalid | component.input-group.border-invalidcolor.status.dangervar(--input-group-border-invalid) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| Focus ring invalid Changes with theme when invalid | component.input-group.focus-ring-invalidcolor.status.dangervar(--input-group-focus-ring-invalid) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| Gap | component.input-group.gapspace.gap.defaultvar(--input-group-gap) | space.16:1rem |
| Padding, top and bottom | component.input-group.padding-ycomponent-spacing-map.x2var(--input-group-padding-y) | space.8:0.5rem |
| Text size | component.input-group.font-sizetype.body.font-sizevar(--input-group-font-size) | font.size.19:1.1875rem |
| Weight | component.input-group.font-weighttype.label.font-weightvar(--input-group-font-weight) | font.weight.700:700 |
| Text Changes with theme | component.input-group.labelcolor.text.mutedvar(--input-group-label) | Lightgray.light.500:#595959Darkgray.dark.500:#949494 |
| Disabled opacity disabled when disabled | component.input-group.disabled-opacity-disabledopacity.disabledvar(--input-group-disabled-opacity-disabled) | opacity.50:0.5 |
| Icon size | component.input-group.icon-sizesize.iconvar(--input-group-icon-size) | size.20:1.25rem |
| Inline | ||
| Start padding left | component.input-group.inline.start.padding-leftspace.inset.control-xvar(--input-group-inline-start-padding-left) | space.24:1.5rem |
| End padding right | component.input-group.inline.end.padding-rightspace.inset.control-xvar(--input-group-inline-end-padding-right) | space.24:1.5rem |
| Block | ||
| Start padding x | component.input-group.block.start.padding-xspace.inset.control-xvar(--input-group-block-start-padding-x) | space.24:1.5rem |
| Start padding top | component.input-group.block.start.padding-topspace.inset.control-xvar(--input-group-block-start-padding-top) | space.24:1.5rem |
| Start padding top 2 | component.input-group.block.start.padding-top-2component-spacing-map.x3var(--input-group-block-start-padding-top-2) | space.20:1.25rem |
| Start padding bottom | component.input-group.block.start.padding-bottomspace.inset.control-xvar(--input-group-block-start-padding-bottom) | space.24:1.5rem |
| End padding x | component.input-group.block.end.padding-xspace.inset.control-xvar(--input-group-block-end-padding-x) | space.24:1.5rem |
| End padding bottom | component.input-group.block.end.padding-bottomspace.inset.control-xvar(--input-group-block-end-padding-bottom) | space.24:1.5rem |
| End padding bottom 2 | component.input-group.block.end.padding-bottom-2component-spacing-map.x3var(--input-group-block-end-padding-bottom-2) | space.20:1.25rem |
| End padding top | component.input-group.block.end.padding-topspace.inset.control-xvar(--input-group-block-end-padding-top) | space.24:1.5rem |
| Xs | ||
| Height | component.input-group.xs.heightsize.control.compactvar(--input-group-xs-height) | size.48:3rem |
| Gap | component.input-group.xs.gapspace.gap.tightvar(--input-group-xs-gap) | space.12:0.75rem |
| Padding, left and right | component.input-group.xs.padding-xcomponent-spacing-map.x2var(--input-group-xs-padding-x) | space.8:0.5rem |
| Sm | ||
| Height | component.input-group.sm.heightsize.control.defaultvar(--input-group-sm-height) | size.52:3.25rem |
| Gap | component.input-group.sm.gapcomponent-spacing-map.x2var(--input-group-sm-gap) | space.8:0.5rem |
| Corner | component.input-group.sm.radiusradius.controlvar(--input-group-sm-radius) | radius.6:0.375rem |
| Padding, left and right | component.input-group.sm.padding-xcomponent-spacing-map.x3var(--input-group-sm-padding-x) | space.20:1.25rem |
| Icon | ||
| Sm size | component.input-group.icon.sm.sizesize.control.defaultvar(--input-group-icon-sm-size) | size.52:3.25rem |
| Control | ||
| Padding, top and bottom | component.input-group.control.padding-yspace.inset.control-xvar(--input-group-control-padding-y) | space.24:1.5rem |
| From the system | ||
| Edge Changes with theme | color.border.default straight from the system, through --border | Lightgray.light.200 · #D6D6D6Darkgray.dark.100 · #222222 |