Bar

.bar is a flex row with align-items: center and a .75rem gap, and it does not wrap. Three declarations. Its value is entirely in what it does not do: a bar stays one line, so a header does not suddenly become two rows tall when somebody adds a button.

When to use it

Use it when the row is a single line by definition: a card header, a toolbar, a table's title row, a panel header with a control at the end. If wrapping would look like a bug rather than a feature, you want a bar.

One line, whatever the width
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> </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>
</div>

.push

.push is margin-inline-start: auto. An auto margin in a flex row absorbs all the free space, so the element it is on — and everything after it — is shoved to the far end.

It is the better alternative to justify-content: space-between for two reasons. It works with any number of children, and you choose where the split happens instead of it always falling between the first item and the rest.

Two items, then .push, then two more — the gap opens where you put it
Q1 exports 12 files
<div class="bar" style="border:1px dashed var(--line);padding:var(--space-3)"> <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)">
  <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>

.push is a standalone utility in src/09-utilities.css, so it works in any flex row, not only inside .bar. .card-header, .menu-item and .sidebar-link all rely on it, and the last two carry their own copy of the rule so the pushed element also picks up a muted colour.

Invoice INV-2291

Paid

.card-header is a flex row, so .push works there too.

<div class="card" style="max-inline-size:26rem"> <div class="card-header"> <h3 class="card-title">Invoice INV-2291</h3> <span class="badge badge-good push">Paid</span> </div> <div class="card-body"><p class="text-muted">.card-header is a flex row, so .push works there too.</p></div> </div>
<div class="card" style="max-inline-size:26rem">
  <div class="card-header">
    <h3 class="card-title">Invoice INV-2291</h3>
    <span class="badge badge-good push">Paid</span>
  </div>
  <div class="card-body"><p class="text-muted">.card-header is a flex row, so .push works there too.</p></div>
</div>

What happens when it does not fit

A bar does not wrap, so when it runs out of room the items squash — and then, past a point, overflow. The point where squashing stops is the thing to know about: a flex item's minimum size is its content, so text with no break opportunity in it will not squash at all. It overflows, and takes the page's horizontal scrollbar with it.

The fix is to nominate the child that should give way, with min-inline-size: 0 to allow the shrink and .truncate to decide what happens to the text.

The name gives way; the badge keeps its size
a-very-long-export-filename-2026-03-11.csv 2.4 MB
<div class="bar" style="max-inline-size:20rem;border:1px dashed var(--line);padding:var(--space-3)"> <span class="truncate" style="min-inline-size:0">a-very-long-export-filename-2026-03-11.csv</span> <span class="badge push">2.4 MB</span> </div>
<div class="bar" style="max-inline-size:20rem;border:1px dashed var(--line);padding:var(--space-3)">
  <span class="truncate" style="min-inline-size:0">a-very-long-export-filename-2026-03-11.csv</span>
  <span class="badge push">2.4 MB</span>
</div>
How each row primitive handles running out of space
ClassOut of roomReach for it when
.bar Squashes, then overflows The row is one line by definition
.cluster Wraps to the next line The number of items varies
.scroller Scrolls sideways, snapping The items are cards and there are many

Gap

.bar is the one layout primitive that does not read --gap: it hard-codes gap: var(--space-3). So the spacing scale classes have no effect on it, and changing a bar's gap means setting gap directly.

That is an inconsistency rather than a decision — .stack, .cluster, .grid, .split, .scroller and .center all take --gap and .bar does not. It is recorded in FINDINGS.md; the one-word fix is to wrap the value in var(--gap, …) like its siblings.

Wider gap set with gap, not --gap
<div class="bar" style="gap:var(--space-6);border:1px dashed var(--line);padding:var(--space-3)"> <strong>Wider gap</strong> <span class="badge push">set with gap, not --gap</span> </div>
<div class="bar" style="gap:var(--space-6);border:1px dashed var(--line);padding:var(--space-3)">
  <strong>Wider gap</strong>
  <span class="badge push">set with gap, not --gap</span>
</div>

Classes

.bar has one member rule — .bar > .push — and no .bar-* variants, so the extractor does not treat it as a component root and there is no completeness check on this page.

Classes, generated from the stylesheet source
Class Layer What it does Source
.bar deck.layout Push the last child away from the rest 04-layout.css:94
.push deck.utilities Pushes this element and everything after it to the far end of a flex row. The one-class replacement for a spacer div. 09-utilities.css:36

Tokens

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

Accessibility

  • No semantics. A bar is a <div> with display: flex. A toolbar that is genuinely a toolbar — a group of controls a reader arrows between — needs role="toolbar" and its own key handling, which Deck does not provide.
  • .push does not reorder. It moves items without changing their DOM order, so the tab order still matches what is on screen. That is the reason to prefer it over order, which does not.
  • Not wrapping is a reflow risk. At 320px or 400% zoom a bar with several items will squash and then overflow, and horizontal scrolling of the page fails WCAG's reflow criterion. Nominate a child to truncate, move items into an overflow menu, or use .cluster and let it wrap.
  • Truncated text needs to be available somewhere. .truncate clips visually and leaves the full string in the DOM, so a screen reader still hears all of it — but a sighted reader cannot. Add a title attribute, or make sure the full value appears elsewhere.
  • Icon-only buttons in a bar need labels. Nothing about the layout supplies an accessible name; each .btn-icon needs its own aria-label.

Right to left

.push is margin-inline-start, so it pushes toward the end edge in whichever direction the text runs. Nothing else about a bar is directional.

The push moves to the left, because that is the end edge here
تصدير الربع الأول ١٢ ملفًا
<div dir="rtl" class="bar" style="border:1px dashed var(--line);padding:var(--space-3)"> <strong>تصدير الربع الأول</strong> <span class="badge">١٢ ملفًا</span> <button class="btn btn-sm push">مشاركة</button> </div>
<div dir="rtl" class="bar" style="border:1px dashed var(--line);padding:var(--space-3)">
  <strong>تصدير الربع الأول</strong>
  <span class="badge">١٢ ملفًا</span>
  <button class="btn btn-sm push">مشاركة</button>
</div>

Reduced motion

Nothing here animates or transitions, so prefers-reduced-motion changes nothing.

Printing

No print rule, and none is needed: a non-wrapping flex row prints as it appears. The caveat is the same as on screen — if the bar is squashed or overflowing at the printed width, it prints that way. A bar that has to survive print should have a truncating child rather than relying on the viewport being wide.

Overriding it

@layer app.components {
  /* Make it read --gap like every other primitive */
  .bar { gap: var(--gap, var(--space-3)); }

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

  /* Let it wrap after all, at small sizes only */
  @media (max-width: 30rem) {
    .bar { flex-wrap: wrap; }
  }
}

That last one is worth thinking twice about. A bar that wraps on a phone is a cluster; if that is what you want at every size, use .cluster and skip the media query.

When not to use it

  • Not when the item count varies. A row built from user data — tags, filters, collaborators — will eventually be too long. Use .cluster.
  • Not for many items. A bar with twelve chips squashes all of them. Use .scroller.
  • Not as a navigation bar. Use .navbar, which has the brand slot, the mobile behaviour and the right height. .bar is a layout primitive, not a chrome component.
  • Not for a form's actions. Use .form-actions, which wraps and has the sticky variant for long forms on a phone.
  • Not with .push on more than one child. Two auto margins split the free space between them, which produces a three-way distribution that is hard to predict and harder to explain. If you want that, use justify-content: space-between and say so.