Cluster

.cluster is a flex row that wraps. That one word is the component: a row of buttons, chips or filters that runs out of room drops onto a second line instead of pushing the page sideways. Its default gap is .75rem, tighter than a stack's, because things side by side need less air than things stacked.

When to use it

Use it for any horizontal group whose length you do not control: action buttons, tags, a breadcrumb, a toolbar. align-items: center is set, so items of different heights — a button beside a badge beside a line of text — line up on their middles without any help.

Narrow the window: it wraps rather than overflowing
Draft saved
<div class="cluster"> <button class="btn btn-primary">Save</button> <button class="btn">Duplicate</button> <button class="btn btn-ghost">Cancel</button> <span class="badge badge-good">Draft saved</span> </div>
<div class="cluster">
  <button class="btn btn-primary">Save</button>
  <button class="btn">Duplicate</button>
  <button class="btn btn-ghost">Cancel</button>
  <span class="badge badge-good">Draft saved</span>
</div>

Wrapping is why a cluster is the safe default on a phone. It is also why .nav-link sets white-space: nowrap: a cluster will happily break a two-word label onto two lines, which is right for a paragraph and wrong for a navigation item.

Alignment

Three variants, each setting justify-content and nothing else. They change where the items sit along the row; they never change the gap or the wrapping.

Default — packed to the start

<div class="cluster" style="border:1px dashed var(--line);padding:var(--space-3)"> <button class="btn btn-sm">One</button> <button class="btn btn-sm">Two</button> <button class="btn btn-sm">Three</button> </div>
<div class="cluster" style="border:1px dashed var(--line);padding:var(--space-3)">
  <button class="btn btn-sm">One</button>
  <button class="btn btn-sm">Two</button>
  <button class="btn btn-sm">Three</button>
</div>

Packed to the end

flex-end, which follows the writing direction — so it is the right edge in English and the left edge in Arabic.

<div class="cluster cluster-end" style="border:1px dashed var(--line);padding:var(--space-3)"> <button class="btn btn-sm">Cancel</button> <button class="btn btn-sm btn-primary">Save</button> </div>
<div class="cluster cluster-end" style="border:1px dashed var(--line);padding:var(--space-3)">
  <button class="btn btn-sm">Cancel</button>
  <button class="btn btn-sm btn-primary">Save</button>
</div>

Centred

<div class="cluster cluster-center" style="border:1px dashed var(--line);padding:var(--space-3)"> <button class="btn btn-sm">One</button> <button class="btn btn-sm">Two</button> </div>
<div class="cluster cluster-center" style="border:1px dashed var(--line);padding:var(--space-3)">
  <button class="btn btn-sm">One</button>
  <button class="btn btn-sm">Two</button>
</div>

Space between

Pushes the first item to one end and the last to the other. With exactly two children this is the classic title-and-action row.

Team members

<div class="cluster cluster-between" style="border:1px dashed var(--line);padding:var(--space-3)"> <h3>Team members</h3> <button class="btn btn-sm btn-primary">Invite</button> </div>
<div class="cluster cluster-between" style="border:1px dashed var(--line);padding:var(--space-3)">
  <h3>Team members</h3>
  <button class="btn btn-sm btn-primary">Invite</button>
</div>

space-between and wrapping interact badly. When the row wraps, each line is justified separately, so a trailing item alone on line two jumps to the far end and looks stranded. If the row can wrap and you want an end-aligned action, reach for .bar and .push instead.

Gap

.cluster reads the same --gap as every other layout primitive, so the whole spacing scale works on it: .stack-2 on a cluster sets a .5rem gap exactly as it would on a stack.

.cluster-tight exists as a readable name for step 2, which is what most clusters want. It is the only place in Deck where a gap has a word instead of a number, and that inconsistency is deliberate but not defensible — it is recorded in FINDINGS.md.

Tight, then step 6 — the step classes work on a cluster unchanged
OverdueThis weekUnassigned
OverdueThis weekUnassigned
<div class="stack stack-3"> <div class="cluster cluster-tight"> <span class="chip">Overdue</span><span class="chip">This week</span><span class="chip">Unassigned</span> </div> <div class="cluster stack-6"> <span class="chip">Overdue</span><span class="chip">This week</span><span class="chip">Unassigned</span> </div> </div>
<div class="stack stack-3">
  <div class="cluster cluster-tight">
    <span class="chip">Overdue</span><span class="chip">This week</span><span class="chip">Unassigned</span>
  </div>
  <div class="cluster stack-6">
    <span class="chip">Overdue</span><span class="chip">This week</span><span class="chip">Unassigned</span>
  </div>
</div>

Both row-gap and column-gap come from the same value, so a wrapped cluster has the same space between its lines as between its items. That is usually right and occasionally too much; set row-gap separately if a wrapped toolbar looks loose.

The row that does not wrap

.bar is the same idea with the wrapping removed: a flex row, align-items: center, a fixed .75rem gap. Use it when the row is a single line by definition — a card header, a toolbar with an overflow menu — and see its own page for .push and for what happens when a bar runs out of room.

Three items, then .push, then two more — the split is where you put it
Q1 exports 12 files
<div class="bar" style="border:1px dashed var(--line);padding:var(--space-3)"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#folder"></use></svg> <strong>Q1 exports</strong> <span class="badge">12 files</span> <button class="btn btn-sm push">Share</button> <button class="btn btn-sm btn-ghost btn-icon" aria-label="More"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#more-horizontal"></use></svg> </button> </div>
<div class="bar" style="border:1px dashed var(--line);padding:var(--space-3)">
  <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#folder"></use></svg>
  <strong>Q1 exports</strong>
  <span class="badge">12 files</span>
  <button class="btn btn-sm push">Share</button>
  <button class="btn btn-sm btn-ghost btn-icon" aria-label="More">
    <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#more-horizontal"></use></svg>
  </button>
</div>

What happens when it does not fit

This is the difference that matters when you are choosing between the three horizontal primitives:

How each row primitive handles running out of space
ClassOut of roomReach for it when
.cluster Wraps to the next line The number of items varies, or is set by a user
.bar Squashes, then overflows The row is one line by definition
.scroller Scrolls sideways, snapping The items are cards and there are many

.bar squashing is the one to watch, and it is covered on its own page: a flex item's minimum size is its content, so a long unbroken string does not squash — it overflows, and takes the page with it.

Classes

Generated from src/04-layout.css. .push is listed because this page documents it; the extractor puts it in deck.utilities, not in the cluster component.

Classes, generated from the stylesheet source
Class Layer What it does Source
.cluster deck.layout Horizontal group that wraps instead of overflowing on a phone 04-layout.css:74
.cluster-between deck.layout First item to one end, last to the other. Beware of combining it with wrapping: each line is justified separately, so a lone item on the second line jumps to the far end and looks stranded. .bar with .push is the predictable version. 04-layout.css:91
.cluster-center deck.layout Centres the row. 04-layout.css:86
.cluster-end deck.layout Packs the row against the end edge, which follows the writing direction rather than always being the right. 04-layout.css:84
.cluster-tight deck.layout Kept as the readable name for step 2, which is what most clusters want 04-layout.css:81

Tokens

Design tokens this component reads
Token Live Declared value
--space-2 .5rem
--space-3 .75rem
--space-4 1rem
--space-6 1.5rem

Accessibility

  • No semantics, as with every layout primitive. A cluster of links that is navigation needs a <nav> with a label; a cluster of tags that is a list needs <ul>.
  • Visual order is DOM order, and must stay that way. .cluster-end and .cluster-between move the whole group; they never reorder items. Do not reach for order to put the primary button last visually — the tab order would then not match, and a keyboard user would tab to Cancel first.
  • .push is safe in this respect because it moves items without reordering them: everything after it moves as a block, in markup order.
  • Wrapping is an accessibility feature. At 200% zoom or a 320px viewport, a row that wraps stays usable and a row that overflows hides its last item off-screen. WCAG's reflow criterion is about exactly this.
  • The gap is the only thing separating adjacent controls. Two buttons .75rem apart are distinguishable; .cluster-tight at .5rem is close to the point where an imprecise tap hits the wrong one. Keep .cluster-tight for chips and badges rather than for destructive actions.
  • A cluster of icon-only buttons needs labels. Nothing about the layout supplies a name; each .btn-icon needs its own aria-label.

Right to left

justify-content: flex-end follows the writing direction, and .push is margin-inline-start, so both mirror with no extra rules. A cluster is one of the components where writing logically costs nothing and buys the whole behaviour.

End alignment and .push both follow the direction
تصدير الربع الأول
<div dir="rtl" class="stack stack-3"> <div class="cluster cluster-end" style="border:1px dashed var(--line);padding:var(--space-3)"> <button class="btn btn-sm">إلغاء</button> <button class="btn btn-sm btn-primary">حفظ</button> </div> <div class="bar" style="border:1px dashed var(--line);padding:var(--space-3)"> <strong>تصدير الربع الأول</strong> <button class="btn btn-sm push">مشاركة</button> </div> </div>
<div dir="rtl" class="stack stack-3">
  <div class="cluster cluster-end" style="border:1px dashed var(--line);padding:var(--space-3)">
    <button class="btn btn-sm">إلغاء</button>
    <button class="btn btn-sm btn-primary">حفظ</button>
  </div>
  <div class="bar" style="border:1px dashed var(--line);padding:var(--space-3)">
    <strong>تصدير الربع الأول</strong>
    <button class="btn btn-sm push">مشاركة</button>
  </div>
</div>

Reduced motion

Nothing here animates or transitions. Wrapping happens at layout time and is not a transition, so a cluster reflowing as the window narrows is not something prefers-reduced-motion has any say over.

Printing

No print rule. A wrapping flex row prints as a wrapping flex row, which is correct — unlike .grid and .split, which are unwound to display: block so their content can break across pages.

Overriding it

/* One cluster, no layer needed */
<div class="cluster" style="--gap:var(--space-1)">

@layer app.components {
  /* Tighter lines than items when a toolbar wraps */
  .cluster { row-gap: var(--space-2); }

  /* Align a cluster on its first line of text rather than its middle */
  .cluster-baseline { align-items: baseline; }
}

align-items: baseline is the override worth knowing about: for a row that is mostly text at different sizes — a heading beside a timestamp — baseline alignment reads better than centring, and Deck does not ship it because centring is right for the far more common case of a control beside a label.

When not to use it

  • Not when wrapping would be wrong. A navigation bar whose links drop onto a second line looks broken. Use .bar, and let the overflowing items go into a menu.
  • Not for many items. Twenty chips in a cluster is five wrapped lines. Use .scroller, which keeps them on one line and scrolls.
  • Not for a grid of cards. A cluster wraps but its items keep their natural widths, so the second line does not line up with the first. Use .grid, which gives every item an equal track.
  • Not for a form's field pairs. Use .field-row, which is a grid with the form's gap and a column floor that stacks on a phone. A cluster of fields wraps at unpredictable places.
  • .cluster-between is not a layout for three or more items that can wrap. Once it wraps, each line justifies on its own and the result looks accidental. .bar with .push is the predictable version.