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.
<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.
<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.
<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:
| Class | Out of room | Reach 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.
| 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
| 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-endand.cluster-betweenmove the whole group; they never reorder items. Do not reach fororderto put the primary button last visually — the tab order would then not match, and a keyboard user would tab to Cancel first. -
.pushis 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-tightat .5rem is close to the point where an imprecise tap hits the wrong one. Keep.cluster-tightfor 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-iconneeds its ownaria-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.
<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-betweenis 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..barwith.pushis the predictable version.