17 CSS Dashboard Layouts15 / 17

Pure CSSMIT licensed

Modular Masonry Grid Dashboard

Eight numbered widgets, two packing engines, one toggle — the fairest fight in CSS. Engine A (multi-column) gives true Pinterest masonry but pours DOWN each column: watch the numbers read 1-4-6 vertically. Engine B (grid + dense flow) keeps left-to-right reading order and backfills holes, but quantizes heights to a row rhythm. The numbers ON the cards make each tradeoff visible in two seconds — the comparison every masonry article hand-waves, made interactive.

Published

Live Demo
Try it

The code

<section class="cdl-15" aria-label="Masonry packing comparison demo">
  <div class="cdl-15__stage">
    <header class="cdl-15__head">
      <h2>Widget board</h2>
      <div class="cdl-15__seg" role="group" aria-label="Packing engine">
        <label><input type="radio" name="cdl15" id="cdl15-cols" checked><span>A · columns</span></label>
        <label><input type="radio" name="cdl15" id="cdl15-grid"><span>B · grid dense</span></label>
      </div>
      <p class="cdl-15__note"><span class="cdl-15__note--a">reading order flows <b>down</b> each column</span><span class="cdl-15__note--b">reading order stays <b>left → right</b>, holes backfilled</span></p>
    </header>
    <div class="cdl-15__board">
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">1</b><h3>Sales pulse</h3><p class="cdl-15__big">$12,940</p><p class="cdl-15__txt">Today · ▲ 7% vs Friday</p></article>
      <article class="cdl-15__card" style="--rows:8"><b class="cdl-15__n">2</b><h3>Traffic</h3><div class="cdl-15__bars" aria-hidden="true"><i style="--h:40%"></i><i style="--h:62%"></i><i style="--h:50%"></i><i style="--h:78%"></i><i style="--h:66%"></i><i style="--h:92%"></i></div><p class="cdl-15__txt">Organic up 12% after the changelog post hit the front page.</p></article>
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">3</b><h3>Note to self</h3><p class="cdl-15__txt">Ship the pricing experiment behind a flag Tuesday. Watch AOV, not conversion, for the first 48h.</p></article>
      <article class="cdl-15__card" style="--rows:5"><b class="cdl-15__n">4</b><h3>NPS</h3><p class="cdl-15__big">+61</p></article>
      <article class="cdl-15__card" style="--rows:7"><b class="cdl-15__n">5</b><h3>On-call</h3><p class="cdl-15__txt">Rotation: Priya → Sam → Jo.</p><p class="cdl-15__txt">Quiet week — 2 pages, both auto-resolved. Postmortem for #221 due Thu.</p></article>
      <article class="cdl-15__card" style="--rows:5"><b class="cdl-15__n">6</b><h3>Signups</h3><p class="cdl-15__big">312<small>/wk</small></p></article>
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">7</b><h3>Churn watch</h3><p class="cdl-15__txt">3 accounts idle 21+ days. Playbook emails queued for Monday 09:00.</p></article>
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">8</b><h3>Build</h3><p class="cdl-15__big cdl-15__ok">passing</p><p class="cdl-15__txt">main · 4m ago</p></article>
    </div>
  </div>
</section>
.cdl-15,
.cdl-15 *,
.cdl-15 *::before,
.cdl-15 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cdl-15 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  --bg: oklch(0.975 0.006 55);
  --panel: oklch(1 0 0);
  --edge: oklch(0.9 0.015 55);
  --ink: oklch(0.27 0.02 45);
  --mut: oklch(0.54 0.03 45);
  --acc: oklch(0.62 0.15 45);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cdl-15__stage {
  width: 100%;
  height: 100vh;
  border: 1px solid var(--edge);
  background: linear-gradient(175deg,var(--bg),oklch(0.958 0.01 55));
  padding: 18px;
}

.cdl-15__head {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.cdl-15__head h2 {
  font-size: 16px;
  letter-spacing: -.01em;
  margin-right: auto;
}

.cdl-15__seg {
  display: flex;
  gap: 4px;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 11px;
  padding: 3px;
}

.cdl-15__seg label {
  position: relative;
  cursor: pointer;
}

.cdl-15__seg input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.cdl-15__seg span {
  display: block;
  font-size: 12px;
  font-weight: 700;
  color: var(--mut);
  padding: 7px 13px;
  border-radius: 8px;
  transition: background .15s,color .15s;
}

.cdl-15__seg label:hover span {
  color: var(--ink);
}

.cdl-15__seg label:has(:checked) span {
  background: var(--acc);
  color: oklch(1 0 0);
}

.cdl-15__seg label:has(:focus-visible) span {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cdl-15__note {
  flex-basis: 100%;
  font-size: 12px;
  color: var(--mut);
}

.cdl-15__note b {
  color: var(--acc);
}

.cdl-15__note--a,
.cdl-15__note--b {
  display: none;
}

.cdl-15:has(#cdl15-cols:checked) .cdl-15__note--a {
  display: inline;
}

.cdl-15:has(#cdl15-grid:checked) .cdl-15__note--b {
  display: inline;
}

.cdl-15:has(#cdl15-cols:checked) .cdl-15__board {
  columns: 3 240px;
  column-gap: 14px;
}

.cdl-15:has(#cdl15-cols:checked) .cdl-15__card {
  break-inside: avoid;
  margin-bottom: 14px;
}

.cdl-15:has(#cdl15-grid:checked) .cdl-15__board {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fill,minmax(min(240px,100%),1fr));
  grid-auto-rows: 8px;
  grid-auto-flow: dense;
}

.cdl-15:has(#cdl15-grid:checked) .cdl-15__card {
  grid-row: span var(--rows);
}

.cdl-15__card {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 14px;
  padding: 15px 16px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  overflow: hidden;
  transition: border-color .2s,box-shadow .2s;
}

.cdl-15__card:hover {
  border-color: color-mix(in oklch,var(--acc) 50%,var(--edge));
  box-shadow: 0 10px 24px -18px oklch(0.4 0.08 45/.6);
}

.cdl-15__n {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 800;
  color: var(--acc);
  background: color-mix(in oklch,var(--acc) 12%,white);
  border-radius: 7px;
}

.cdl-15__card h3 {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--mut);
}

.cdl-15__big {
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}

.cdl-15__big small {
  font-size: 13px;
  color: var(--mut);
  font-weight: 600;
}

.cdl-15__ok {
  color: oklch(0.55 0.14 155);
}

.cdl-15__txt {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--mut);
}

.cdl-15__bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 56px;
}

.cdl-15__bars i {
  flex: 1;
  height: var(--h);
  border-radius: 3px 3px 0 0;
  background: linear-gradient(to top,color-mix(in oklch,var(--acc) 30%,transparent),var(--acc));
}

@media (prefers-reduced-motion: reduce) {
  .cdl-15 * {
    transition: none !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Dashboard Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Modular Masonry Grid Dashboard
Source: https://codefronts.com/layouts/css-dashboard-layouts/modular-masonry-grid-dashboard/

Eight numbered widgets, two packing engines, one toggle — the fairest fight in CSS. Engine A (multi-column) gives true Pinterest masonry but pours DOWN each column: watch the numbers read 1-4-6 vertically. Engine B (grid + dense flow) keeps left-to-right reading order and backfills holes, but quantizes heights to a row rhythm. The numbers ON the cards make each tradeoff visible in two seconds — the comparison every masonry article hand-waves, made interactive.
## HTML
```html
<section class="cdl-15" aria-label="Masonry packing comparison demo">
  <div class="cdl-15__stage">
    <header class="cdl-15__head">
      <h2>Widget board</h2>
      <div class="cdl-15__seg" role="group" aria-label="Packing engine">
        <label><input type="radio" name="cdl15" id="cdl15-cols" checked><span>A · columns</span></label>
        <label><input type="radio" name="cdl15" id="cdl15-grid"><span>B · grid dense</span></label>
      </div>
      <p class="cdl-15__note"><span class="cdl-15__note--a">reading order flows <b>down</b> each column</span><span class="cdl-15__note--b">reading order stays <b>left → right</b>, holes backfilled</span></p>
    </header>
    <div class="cdl-15__board">
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">1</b><h3>Sales pulse</h3><p class="cdl-15__big">$12,940</p><p class="cdl-15__txt">Today · ▲ 7% vs Friday</p></article>
      <article class="cdl-15__card" style="--rows:8"><b class="cdl-15__n">2</b><h3>Traffic</h3><div class="cdl-15__bars" aria-hidden="true"><i style="--h:40%"></i><i style="--h:62%"></i><i style="--h:50%"></i><i style="--h:78%"></i><i style="--h:66%"></i><i style="--h:92%"></i></div><p class="cdl-15__txt">Organic up 12% after the changelog post hit the front page.</p></article>
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">3</b><h3>Note to self</h3><p class="cdl-15__txt">Ship the pricing experiment behind a flag Tuesday. Watch AOV, not conversion, for the first 48h.</p></article>
      <article class="cdl-15__card" style="--rows:5"><b class="cdl-15__n">4</b><h3>NPS</h3><p class="cdl-15__big">+61</p></article>
      <article class="cdl-15__card" style="--rows:7"><b class="cdl-15__n">5</b><h3>On-call</h3><p class="cdl-15__txt">Rotation: Priya → Sam → Jo.</p><p class="cdl-15__txt">Quiet week — 2 pages, both auto-resolved. Postmortem for #221 due Thu.</p></article>
      <article class="cdl-15__card" style="--rows:5"><b class="cdl-15__n">6</b><h3>Signups</h3><p class="cdl-15__big">312<small>/wk</small></p></article>
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">7</b><h3>Churn watch</h3><p class="cdl-15__txt">3 accounts idle 21+ days. Playbook emails queued for Monday 09:00.</p></article>
      <article class="cdl-15__card" style="--rows:6"><b class="cdl-15__n">8</b><h3>Build</h3><p class="cdl-15__big cdl-15__ok">passing</p><p class="cdl-15__txt">main · 4m ago</p></article>
    </div>
  </div>
</section>
```
## CSS
```css
.cdl-15,
.cdl-15 *,
.cdl-15 *::before,
.cdl-15 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cdl-15 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  --bg: oklch(0.975 0.006 55);
  --panel: oklch(1 0 0);
  --edge: oklch(0.9 0.015 55);
  --ink: oklch(0.27 0.02 45);
  --mut: oklch(0.54 0.03 45);
  --acc: oklch(0.62 0.15 45);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cdl-15__stage {
  width: 100%;
  height: 100vh;
  border: 1px solid var(--edge);
  background: linear-gradient(175deg,var(--bg),oklch(0.958 0.01 55));
  padding: 18px;
}

.cdl-15__head {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.cdl-15__head h2 {
  font-size: 16px;
  letter-spacing: -.01em;
  margin-right: auto;
}

.cdl-15__seg {
  display: flex;
  gap: 4px;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 11px;
  padding: 3px;
}

.cdl-15__seg label {
  position: relative;
  cursor: pointer;
}

.cdl-15__seg input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.cdl-15__seg span {
  display: block;
  font-size: 12px;
  font-weight: 700;
  color: var(--mut);
  padding: 7px 13px;
  border-radius: 8px;
  transition: background .15s,color .15s;
}

.cdl-15__seg label:hover span {
  color: var(--ink);
}

.cdl-15__seg label:has(:checked) span {
  background: var(--acc);
  color: oklch(1 0 0);
}

.cdl-15__seg label:has(:focus-visible) span {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cdl-15__note {
  flex-basis: 100%;
  font-size: 12px;
  color: var(--mut);
}

.cdl-15__note b {
  color: var(--acc);
}

.cdl-15__note--a,
.cdl-15__note--b {
  display: none;
}

.cdl-15:has(#cdl15-cols:checked) .cdl-15__note--a {
  display: inline;
}

.cdl-15:has(#cdl15-grid:checked) .cdl-15__note--b {
  display: inline;
}

.cdl-15:has(#cdl15-cols:checked) .cdl-15__board {
  columns: 3 240px;
  column-gap: 14px;
}

.cdl-15:has(#cdl15-cols:checked) .cdl-15__card {
  break-inside: avoid;
  margin-bottom: 14px;
}

.cdl-15:has(#cdl15-grid:checked) .cdl-15__board {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fill,minmax(min(240px,100%),1fr));
  grid-auto-rows: 8px;
  grid-auto-flow: dense;
}

.cdl-15:has(#cdl15-grid:checked) .cdl-15__card {
  grid-row: span var(--rows);
}

.cdl-15__card {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--edge);
  border-radius: 14px;
  padding: 15px 16px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  overflow: hidden;
  transition: border-color .2s,box-shadow .2s;
}

.cdl-15__card:hover {
  border-color: color-mix(in oklch,var(--acc) 50%,var(--edge));
  box-shadow: 0 10px 24px -18px oklch(0.4 0.08 45/.6);
}

.cdl-15__n {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 800;
  color: var(--acc);
  background: color-mix(in oklch,var(--acc) 12%,white);
  border-radius: 7px;
}

.cdl-15__card h3 {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--mut);
}

.cdl-15__big {
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}

.cdl-15__big small {
  font-size: 13px;
  color: var(--mut);
  font-weight: 600;
}

.cdl-15__ok {
  color: oklch(0.55 0.14 155);
}

.cdl-15__txt {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--mut);
}

.cdl-15__bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 56px;
}

.cdl-15__bars i {
  flex: 1;
  height: var(--h);
  border-radius: 3px 3px 0 0;
  background: linear-gradient(to top,color-mix(in oklch,var(--acc) 30%,transparent),var(--acc));
}

@media (prefers-reduced-motion: reduce) {
  .cdl-15 * {
    transition: none !important;
  }
}
```

How this works

Engine A is three lines. Columns were built for newspaper text, and masonry is just cards as text:

.board--columns{ columns: 3 240px; column-gap:14px; }
.card{ break-inside:avoid; margin-bottom:14px; }

columns: 3 240px means “at most 3 columns, each at least 240px” — responsive column count for free. Heights stay natural, gaps are perfect… but item 2 renders BELOW item 1, not beside it. Fine for feeds and moodboards; wrong for scan-by-recency dashboards.

Engine B keeps DOM order horizontal by quantizing: a fine row grid (grid-auto-rows:8px) where each card spans roughly its content height in 8px beats, and dense lets later small cards backfill earlier holes:

.board--grid{ display:grid; gap:14px;
  grid-template-columns: repeat(auto-fill, minmax(min(240px,100%),1fr));
  grid-auto-rows: 8px; grid-auto-flow: dense;
}
.card{ grid-row: span var(--rows); }  /* span ≈ (height+gap) ÷ (rowHeight+gap) */

The demo pre-computes each card's --rows; dynamic content would measure once in JS (Math.ceil((card.scrollHeight + 14) / (8 + 14)) — height plus gap over row plus gap) — the one line separating this from a library. The toggle is two radios and :has() swapping the board's class-equivalent state. Native grid-template-rows: masonry (and the newer item-flow proposal) will eventually make both engines one line — track it; don't ship it yet.

Make it yours

  • Column width for both engines: the shared 240px floor token.
  • Engine B's resolution: grid-auto-rows:8px — smaller beats hug content tighter but multiply row count; 4–10px is the sweet spot.
  • Turn OFF dense if strict visual chronology matters more than tight packing (holes become honest).
  • Pin a hero widget with grid-column:span 2 in engine B — spans and masonry-ish packing compose.

Gotchas — read before shipping

  • Multi-column reorders reading + tab order visually (DOM order is preserved for AT, which is exactly the mismatch) — avoid engine A when order carries meaning or heavy interactivity.
  • In engine B the --rows span must track content: fixed spans + user-generated text = clipped or gappy cards. Measure on render, or let cards own inner scroll areas.
  • break-inside:avoid is mandatory in engine A or cards split across columns mid-paragraph; Safari also wants -webkit-column-break-inside:avoid on very old versions.

Browser support

ChromeSafariFirefoxEdge
111+16.2+121+111+

Multi-column: 2011. Dense grid flow: 2017. :has() for the toggle: Firefox 121. Native grid-template-rows:masonry remains experimental (Firefox flag / Safari TP) — this demo intentionally ships the two techniques that work today.

Techniques used in this demo

Search CodeFronts

Loading…