Input group

.input-group

Input group — a control and the button that acts on it, welded into one box.

Examples

A field and the button that acts on it

View “A field and the button that acts on it” as
<div class="field">
  <label class="field-label" for="ig-station">Station</label>
  <div class="input-group">
    <input class="input" id="ig-station" type="search" placeholder="Milano…" />
    <button class="btn btn-primary" type="button">Search</button>
  </div>
</div>

One control, welded: the shared edge is drawn once and the two outer corners are the only rounded ones. Two consumers had written the arithmetic by hand.

With an icon-only button

View “With an icon-only button” as
<div class="field">
  <label class="field-label" for="ig-number">Train number</label>
  <div class="input-group">
    <input class="input" id="ig-number" type="text" placeholder="2467" />
    <button class="btn btn-primary btn-icon" type="button" aria-label="Search">
      <svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg>
    </button>
  </div>
</div>

An icon-only button carries its name on the control, never on the <svg> — the glyph is aria-hidden and the button has the aria-label.

Tokens 1

Level 2, declared on .input-group itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by input-group
TokenDefault
--input-group-overlapvar(--border-width)

Variants and states

Variants

None. It is one shape, and the page does the rest.

Inside it

  • .combobox
  • .input
  • .search
  • .search-box
  • .select
  • .suggest
  • .btn

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :focus

Before you ship it

What you have to do 4 requirements

  1. Label the field, and name the button — and put the label outside the group. The weld makes the button look like part of the field and it is not: an icon-only button carries its own aria-label, and the field keeps a real <label>. Where the design has no room for label text the label is still a real element, .sr-only, in the .field above — not in the group. Inside it, it is a child that draws nothing, and the seam rules below are written so that this costs nothing anyway; keeping it out is what makes the markup say what it means.
  2. Keep the button's block padding out of the way. .btn and .input share the same --target-size floor, so they are the same height until the button adds its own block padding around a line of text and grows past it. .btn-icon has none and needs nothing; a labelled button in a group gets --btn-padding-block: 0, which this file writes for it.
  3. An item is .input, .select, .btn, or one of the four wrappers — .suggest, .search, .search-box, .combobox, which are transparent here: the rules reach one level in to find the .input or .select that actually draws the border. Anything else in the group — a hidden label, a hidden input — is not an item and does not move the seam. Anything deeper than one level, or any other wrapper, squares its own corners.
  4. Two items, or three. Nothing stops more, but a field flanked on both sides is a form, not a control.

src/css/components/input-group.css · npx mostlycss add input-group