Actual CSS

Joined Controls

Use .join when controls need to be visually joined into a single unit.

Related terms: button group, input group, toolbar.

.join groups controls that keep their own identity and function. For one choice among a few on a shared track, use a segmented control.

Class reference

ClassKindDescription
.joinCompositionJoins adjacent controls into a single unit.
.join-addonCompositionStatic prefix or suffix content.

Add .join to the wrapper that groups the controls. Use .join-addon for static prefix/suffix content such as currency symbols, units, or protocol text. Action buttons inside the group should use .btn. The wrapper should still carry role="group" with a label for accessibility.

Each child keeps its own variant. Use the same variant across the group, and change it only to mark state, such as a solid pressed toggle among outlines.

When composing source files manually, import components/join.css after the controls it groups so joined corners take precedence.

$
Enter the invoice total before tax.
<div class="field">
  <label class="field-label" id="amount-label" for="amount">Amount</label>
  <div class="join" role="group" aria-labelledby="amount-label">
    <span class="join-addon">lt;/span>
    <input class="input" id="amount" name="amount" inputmode="decimal" />
    <button class="btn outline" type="button">Clear</button>
  </div>
  <span class="field-help">Enter the invoice total before tax.</span>
</div>

.join handles border-radius and border collapsing between adjacent children. It works with any direct child — .input, .btn, .select, or .join-addon — plus one wrapper level (e.g. a combobox element around the native control): the wrapper receives the group corners and the inner control inherits them via border-radius: inherit. When an .input, .textarea, or .select receives keyboard focus, it keeps its own inset focus outline; the group never moves the ring onto itself, so the focused segment remains identifiable.

Linting role="group"

Biome's a11y/useSemanticElements reports the wrapper and offers <fieldset> as the replacement. That is the only element it suggests, and it does not fit: <fieldset> groups form controls under a <legend>, while ARIA's group is any labelled set of related objects. Suppress it on the element:

<!-- biome-ignore lint/a11y/useSemanticElements: ARIA group has no HTML
     equivalent here; fieldset expects form controls under a legend. -->
<div class="join" role="group" aria-label="Date navigation">

Unlike the menu rule in flyout, do not switch this one off globally: it correctly catches <div role="navigation">, <div role="button"> and <div role="separator">. And drop role="group" rather than suppressing it when the wrapper has no label to give — an unnamed group adds a boundary and nothing else.

CSS hooks

  • --join-radius — outer corner radius of the joined group. The per-corner values of each child are derived from it.
Actual CSS

Search documentation