Actual CSS

Joined Controls

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

Related terms: segmented control, button group, input group.

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.

.join reshapes corners and shared edges; it does not unify the segments' treatments, and the children keep whatever classes they carry. Give them one variant family so the group reads as a single unit, and change a segment's variant only to mark state — a solid button among outlines for the active segment of a segmented control. A group mixing .btn, .btn outline and .btn soft is valid markup that renders as three unrelated controls stuck together.

When composing source files manually, import components/join.css after the controls it groups. .join writes child border-radius longhands directly so joined corners win at equal specificity.

$
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. When an .input, .textarea, or .select receives keyboard-relevant focus, the focus ring surrounds the complete joined field. Attached buttons keep their own focus indicator when reached directly, so the actionable 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