15 / 17
All 17 in 17 Tailwind CSS Dashboard Layouts
- 01Tailwind CSS Bento Grid Dashboard Layout
- 02Tailwind CSS Collapsible Sidebar Admin Dashboard
- 03Tailwind CSS Dashboard KPI Stat Cards Grid
- 04Tailwind CSS Master-Detail Split View Inbox Layout
- 05Tailwind CSS Fixed Header Dashboard with Independent Scroll Panels
- 06Tailwind CSS Grid Template Areas Dashboard Layout
- 07Tailwind CSS Responsive Off-Canvas Sidebar Drawer Dashboard
- 08Tailwind CSS Container Query Responsive Dashboard Cards
- 09Tailwind CSS E-commerce Admin Dashboard with Bulk Actions
- 10Tailwind CSS Dark Mode Dashboard with Light / Dark / System Toggle
- 11Tailwind CSS Multi-Level Nested Sidebar Navigation Layout
- 12Minimal Tailwind CSS Admin Dashboard Template (No JavaScript)
- 13Tailwind CSS AI Copilot Chat Sidebar Dashboard Layout
- 14Tailwind CSS Data Table with Sticky Header and Sticky First Column
- 15Tailwind CSS Masonry Grid Dashboard Layout (Columns vs Grid Dense)
- 16Tailwind CSS Full-Screen Analytics Dashboard (100vh, No Scroll)
- 17Tailwind CSS Top Navigation Bar Dashboard Layout with Tabs
Tailwind CSS Masonry Grid Dashboard Layout (Columns vs Grid Dense)
Eight numbered widgets, two packing engines, one switch. Engine A is columns-[3_15rem] + break-inside-avoid: true masonry, but it reads down each column. Engine B is grid-flow-dense auto-rows-2 with row-span-(--rows): left-to-right order with holes backfilled, heights quantised to an 8px beat. The numbers on the cards make the trade-off obvious in two seconds; the switch is a radio pair read by group-has-[#tdl15-grid:checked]/board:.
Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69
Cruip · Mosaic by Cruip · Affiliate link
The code
<section class="tdl-15 w-full min-h-svh bg-tdl15-bg bg-linear-175 from-tdl15-bg to-tdl15-bg2 font-tdl15-sans text-tdl15-ink antialiased" aria-label="Masonry packing comparison demo">
<div class="group/board mx-auto w-full max-w-6xl p-4 sm:p-6">
<header class="mb-4 flex flex-wrap items-center gap-3.5">
<h2 class="mr-auto text-lg font-semibold tracking-tight">Widget board</h2>
<fieldset class="flex gap-1 rounded-xl border border-tdl15-edge bg-tdl15-panel p-0.75 [&>label]:flex [&>label]:h-9 [&>label]:cursor-pointer [&>label]:items-center [&>label]:gap-2 [&>label]:rounded-lg [&>label]:px-3.25 [&>label]:text-xs [&>label]:font-semibold [&>label]:text-tdl15-mut [&>label]:transition [&>label:hover]:text-tdl15-ink [&>label:has(:checked)]:bg-tdl15-acc [&>label:has(:checked)]:text-white [&>label:has(:focus-visible)]:outline-2 [&>label:has(:focus-visible)]:outline-offset-2 [&>label:has(:focus-visible)]:outline-tdl15-acc [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2">
<legend class="sr-only">Packing engine</legend>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-cols" checked><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4v16M12 4v16M19 4v16"/></svg>A · columns</label>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-grid"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="9" rx="1"/><rect x="13" y="4" width="7" height="5" rx="1"/><rect x="4" y="15" width="7" height="5" rx="1"/><rect x="13" y="11" width="7" height="9" rx="1"/></svg>B · grid dense</label>
</fieldset>
<p class="basis-full text-[13px] text-tdl15-mut"><span class="group-has-[#tdl15-grid:checked]/board:hidden">Reading order flows <b class="text-tdl15-accink">down</b> each column — note 1 → 2 → 3 stacking vertically.</span><span class="hidden group-has-[#tdl15-grid:checked]/board:inline">Reading order stays <b class="text-tdl15-accink">left → right</b>; holes are backfilled, heights snap to an 8px beat.</span></p>
</header>
<div class="gap-3.5 group-has-[#tdl15-cols:checked]/board:columns-[3_15rem] group-has-[#tdl15-cols:checked]/board:*:mb-3.5 group-has-[#tdl15-cols:checked]/board:*:break-inside-avoid group-has-[#tdl15-grid:checked]/board:grid group-has-[#tdl15-grid:checked]/board:auto-rows-2 group-has-[#tdl15-grid:checked]/board:grid-flow-dense group-has-[#tdl15-grid:checked]/board:grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))] group-has-[#tdl15-grid:checked]/board:*:row-span-(--rows) *:relative *:flex *:flex-col *:gap-2 *:overflow-hidden *:rounded-2xl *:border *:border-tdl15-edge *:bg-tdl15-panel *:p-4 *:transition *:hover:border-tdl15-acc/50 *:hover:shadow-[0_10px_24px_-18px_var(--color-tdl15-shadow)] [&_h3]:text-[10.5px] [&_h3]:font-semibold [&_h3]:tracking-[0.11em] [&_h3]:text-tdl15-mut [&_h3]:uppercase [&_.n]:absolute [&_.n]:top-2.5 [&_.n]:right-3 [&_.n]:grid [&_.n]:size-6 [&_.n]:place-items-center [&_.n]:rounded-lg [&_.n]:bg-tdl15-acc/12 [&_.n]:text-xs [&_.n]:font-bold [&_.n]:text-tdl15-accink [&_.big]:text-[26px] [&_.big]:font-semibold [&_.big]:tracking-tight [&_.big]:tabular-nums [&_.txt]:text-[13px]/[1.55] [&_.txt]:text-tdl15-mut">
<article style="--rows:6"><b class="n">1</b><h3>Sales pulse</h3><p class="big">$12,940</p><p class="txt">Today · <span class="font-semibold text-tdl15-good">▲ 7%</span> vs Friday</p></article>
<article style="--rows:9"><b class="n">2</b><h3>Traffic</h3><div aria-hidden="true" class="flex h-14 items-end gap-1.5 *:h-(--h) *:flex-1 *:rounded-t-[3px] *:bg-linear-to-t *:from-tdl15-acc/30 *:to-tdl15-acc"><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="txt">Organic up 12% after the changelog post hit the front page.</p></article>
<article style="--rows:7"><b class="n">3</b><h3>Note to self</h3><p class="txt">Ship the pricing experiment behind a flag Tuesday. Watch AOV, not conversion, for the first 48h.</p></article>
<article style="--rows:5"><b class="n">4</b><h3>NPS</h3><p class="big">+61</p><div aria-hidden="true" class="h-1.5 overflow-hidden rounded-full bg-tdl15-track"><i class="block h-full w-[80.5%] rounded-full bg-tdl15-good"></i></div></article>
<article style="--rows:8"><b class="n">5</b><h3>On-call</h3><p class="txt">Rotation: Priya → Sam → Jo.</p><p class="txt">Quiet week — 2 pages, both auto-resolved. Postmortem for #221 due Thu.</p></article>
<article style="--rows:5"><b class="n">6</b><h3>Signups</h3><p class="big">312<small class="text-[13px] font-semibold text-tdl15-mut">/wk</small></p></article>
<article style="--rows:6"><b class="n">7</b><h3>Churn watch</h3><p class="txt">3 accounts idle 21+ days. Playbook emails queued for Monday 09:00.</p></article>
<article style="--rows:6"><b class="n">8</b><h3>Build</h3><p class="big flex items-center gap-2 text-tdl15-good!"><i aria-hidden="true" class="size-2.5 rounded-full bg-tdl15-good"></i>passing</p><p class="txt">main · 4m ago</p></article>
</div>
</div>
</section><section class="tdl-15 w-full min-h-svh bg-tdl15-bg bg-linear-175 from-tdl15-bg to-tdl15-bg2 font-tdl15-sans text-tdl15-ink antialiased" aria-label="Masonry packing comparison demo">
<div class="group/board mx-auto w-full max-w-6xl p-4 sm:p-6">
<header class="mb-4 flex flex-wrap items-center gap-3.5">
<h2 class="mr-auto text-lg font-semibold tracking-tight">Widget board</h2>
<fieldset class="flex gap-1 rounded-xl border border-tdl15-edge bg-tdl15-panel p-0.75 [&>label]:flex [&>label]:h-9 [&>label]:cursor-pointer [&>label]:items-center [&>label]:gap-2 [&>label]:rounded-lg [&>label]:px-3.25 [&>label]:text-xs [&>label]:font-semibold [&>label]:text-tdl15-mut [&>label]:transition [&>label:hover]:text-tdl15-ink [&>label:has(:checked)]:bg-tdl15-acc [&>label:has(:checked)]:text-white [&>label:has(:focus-visible)]:outline-2 [&>label:has(:focus-visible)]:outline-offset-2 [&>label:has(:focus-visible)]:outline-tdl15-acc [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2">
<legend class="sr-only">Packing engine</legend>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-cols" checked><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4v16M12 4v16M19 4v16"/></svg>A · columns</label>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-grid"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="9" rx="1"/><rect x="13" y="4" width="7" height="5" rx="1"/><rect x="4" y="15" width="7" height="5" rx="1"/><rect x="13" y="11" width="7" height="9" rx="1"/></svg>B · grid dense</label>
</fieldset>
<p class="basis-full text-[13px] text-tdl15-mut"><span class="group-has-[#tdl15-grid:checked]/board:hidden">Reading order flows <b class="text-tdl15-accink">down</b> each column — note 1 → 2 → 3 stacking vertically.</span><span class="hidden group-has-[#tdl15-grid:checked]/board:inline">Reading order stays <b class="text-tdl15-accink">left → right</b>; holes are backfilled, heights snap to an 8px beat.</span></p>
</header>
<div class="gap-3.5 group-has-[#tdl15-cols:checked]/board:columns-[3_15rem] group-has-[#tdl15-cols:checked]/board:*:mb-3.5 group-has-[#tdl15-cols:checked]/board:*:break-inside-avoid group-has-[#tdl15-grid:checked]/board:grid group-has-[#tdl15-grid:checked]/board:auto-rows-2 group-has-[#tdl15-grid:checked]/board:grid-flow-dense group-has-[#tdl15-grid:checked]/board:grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))] group-has-[#tdl15-grid:checked]/board:*:row-span-(--rows) *:relative *:flex *:flex-col *:gap-2 *:overflow-hidden *:rounded-2xl *:border *:border-tdl15-edge *:bg-tdl15-panel *:p-4 *:transition *:hover:border-tdl15-acc/50 *:hover:shadow-[0_10px_24px_-18px_var(--color-tdl15-shadow)] [&_h3]:text-[10.5px] [&_h3]:font-semibold [&_h3]:tracking-[0.11em] [&_h3]:text-tdl15-mut [&_h3]:uppercase [&_.n]:absolute [&_.n]:top-2.5 [&_.n]:right-3 [&_.n]:grid [&_.n]:size-6 [&_.n]:place-items-center [&_.n]:rounded-lg [&_.n]:bg-tdl15-acc/12 [&_.n]:text-xs [&_.n]:font-bold [&_.n]:text-tdl15-accink [&_.big]:text-[26px] [&_.big]:font-semibold [&_.big]:tracking-tight [&_.big]:tabular-nums [&_.txt]:text-[13px]/[1.55] [&_.txt]:text-tdl15-mut">
<article style="--rows:6"><b class="n">1</b><h3>Sales pulse</h3><p class="big">$12,940</p><p class="txt">Today · <span class="font-semibold text-tdl15-good">▲ 7%</span> vs Friday</p></article>
<article style="--rows:9"><b class="n">2</b><h3>Traffic</h3><div aria-hidden="true" class="flex h-14 items-end gap-1.5 *:h-(--h) *:flex-1 *:rounded-t-[3px] *:bg-linear-to-t *:from-tdl15-acc/30 *:to-tdl15-acc"><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="txt">Organic up 12% after the changelog post hit the front page.</p></article>
<article style="--rows:7"><b class="n">3</b><h3>Note to self</h3><p class="txt">Ship the pricing experiment behind a flag Tuesday. Watch AOV, not conversion, for the first 48h.</p></article>
<article style="--rows:5"><b class="n">4</b><h3>NPS</h3><p class="big">+61</p><div aria-hidden="true" class="h-1.5 overflow-hidden rounded-full bg-tdl15-track"><i class="block h-full w-[80.5%] rounded-full bg-tdl15-good"></i></div></article>
<article style="--rows:8"><b class="n">5</b><h3>On-call</h3><p class="txt">Rotation: Priya → Sam → Jo.</p><p class="txt">Quiet week — 2 pages, both auto-resolved. Postmortem for #221 due Thu.</p></article>
<article style="--rows:5"><b class="n">6</b><h3>Signups</h3><p class="big">312<small class="text-[13px] font-semibold text-tdl15-mut">/wk</small></p></article>
<article style="--rows:6"><b class="n">7</b><h3>Churn watch</h3><p class="txt">3 accounts idle 21+ days. Playbook emails queued for Monday 09:00.</p></article>
<article style="--rows:6"><b class="n">8</b><h3>Build</h3><p class="big flex items-center gap-2 text-tdl15-good!"><i aria-hidden="true" class="size-2.5 rounded-full bg-tdl15-good"></i>passing</p><p class="txt">main · 4m ago</p></article>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-tdl15-bg: #fbf8f4;
--color-tdl15-bg: oklch(0.978 0.007 60);
--color-tdl15-bg2: #f4efe8;
--color-tdl15-bg2: oklch(0.955 0.012 60);
--color-tdl15-panel: #ffffff;
--color-tdl15-panel: oklch(1 0 0);
--color-tdl15-track: #efe9e2;
--color-tdl15-track: oklch(0.94 0.01 60);
--color-tdl15-edge: #e8e0d6;
--color-tdl15-edge: oklch(0.91 0.015 60);
--color-tdl15-ink: #2f2620;
--color-tdl15-ink: oklch(0.27 0.02 50);
--color-tdl15-mut: #6a5d53;
--color-tdl15-mut: oklch(0.49 0.03 50);
--color-tdl15-acc: #d0612c;
--color-tdl15-acc: oklch(0.62 0.16 45);
--color-tdl15-accink: #a5461a;
--color-tdl15-accink: oklch(0.52 0.15 42);
--color-tdl15-good: #17804a;
--color-tdl15-good: oklch(0.52 0.13 155);
--color-tdl15-shadow: #7a5a44;
--color-tdl15-shadow: oklch(0.45 0.07 50 / 0.55);
--font-tdl15-sans: 'Lexend', system-ui, sans-serif;
}
.tdl-15 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tdl-15 * {
transition: none !important;
}
}@import "tailwindcss";
@theme {
--color-tdl15-bg: #fbf8f4;
--color-tdl15-bg: oklch(0.978 0.007 60);
--color-tdl15-bg2: #f4efe8;
--color-tdl15-bg2: oklch(0.955 0.012 60);
--color-tdl15-panel: #ffffff;
--color-tdl15-panel: oklch(1 0 0);
--color-tdl15-track: #efe9e2;
--color-tdl15-track: oklch(0.94 0.01 60);
--color-tdl15-edge: #e8e0d6;
--color-tdl15-edge: oklch(0.91 0.015 60);
--color-tdl15-ink: #2f2620;
--color-tdl15-ink: oklch(0.27 0.02 50);
--color-tdl15-mut: #6a5d53;
--color-tdl15-mut: oklch(0.49 0.03 50);
--color-tdl15-acc: #d0612c;
--color-tdl15-acc: oklch(0.62 0.16 45);
--color-tdl15-accink: #a5461a;
--color-tdl15-accink: oklch(0.52 0.15 42);
--color-tdl15-good: #17804a;
--color-tdl15-good: oklch(0.52 0.13 155);
--color-tdl15-shadow: #7a5a44;
--color-tdl15-shadow: oklch(0.45 0.07 50 / 0.55);
--font-tdl15-sans: 'Lexend', system-ui, sans-serif;
}
.tdl-15 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tdl-15 * {
transition: none !important;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tdl15-bg: #fbf8f4;
--color-tdl15-bg: oklch(0.978 0.007 60);
--color-tdl15-bg2: #f4efe8;
--color-tdl15-bg2: oklch(0.955 0.012 60);
--color-tdl15-panel: #ffffff;
--color-tdl15-panel: oklch(1 0 0);
--color-tdl15-track: #efe9e2;
--color-tdl15-track: oklch(0.94 0.01 60);
--color-tdl15-edge: #e8e0d6;
--color-tdl15-edge: oklch(0.91 0.015 60);
--color-tdl15-ink: #2f2620;
--color-tdl15-ink: oklch(0.27 0.02 50);
--color-tdl15-mut: #6a5d53;
--color-tdl15-mut: oklch(0.49 0.03 50);
--color-tdl15-acc: #d0612c;
--color-tdl15-acc: oklch(0.62 0.16 45);
--color-tdl15-accink: #a5461a;
--color-tdl15-accink: oklch(0.52 0.15 42);
--color-tdl15-good: #17804a;
--color-tdl15-good: oklch(0.52 0.13 155);
--color-tdl15-shadow: #7a5a44;
--color-tdl15-shadow: oklch(0.45 0.07 50 / 0.55);
--font-tdl15-sans: 'Lexend', system-ui, sans-serif;
}
.tdl-15 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tdl-15 * {
transition: none !important;
}
}@import "tailwindcss";
@theme {
--color-tdl15-bg: #fbf8f4;
--color-tdl15-bg: oklch(0.978 0.007 60);
--color-tdl15-bg2: #f4efe8;
--color-tdl15-bg2: oklch(0.955 0.012 60);
--color-tdl15-panel: #ffffff;
--color-tdl15-panel: oklch(1 0 0);
--color-tdl15-track: #efe9e2;
--color-tdl15-track: oklch(0.94 0.01 60);
--color-tdl15-edge: #e8e0d6;
--color-tdl15-edge: oklch(0.91 0.015 60);
--color-tdl15-ink: #2f2620;
--color-tdl15-ink: oklch(0.27 0.02 50);
--color-tdl15-mut: #6a5d53;
--color-tdl15-mut: oklch(0.49 0.03 50);
--color-tdl15-acc: #d0612c;
--color-tdl15-acc: oklch(0.62 0.16 45);
--color-tdl15-accink: #a5461a;
--color-tdl15-accink: oklch(0.52 0.15 42);
--color-tdl15-good: #17804a;
--color-tdl15-good: oklch(0.52 0.13 155);
--color-tdl15-shadow: #7a5a44;
--color-tdl15-shadow: oklch(0.45 0.07 50 / 0.55);
--font-tdl15-sans: 'Lexend', system-ui, sans-serif;
}
.tdl-15 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tdl-15 * {
transition: none !important;
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lexend:wght@400..700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lexend:wght@400..700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Four
.cdl-15:has(#…:checked) …engine rules becamegroup-has-[…]/board:utilities on the board, including stacked child variants likegroup-has-[#tdl15-cols:checked]/board:*:break-inside-avoid. grid-row: span var(--rows)is v4'srow-span-(--rows)CSS-variable shorthand; the 8px beat isauto-rows-2.- UI upgrade from the CSS original: the segmented control gained engine icons and 36px targets, the note under it now points at what to look for (1 → 2 → 3), and the NPS and Build widgets gained a meter and a status dot.
- Fix: card spans were re-tuned so no widget clips its content in Engine B (the Traffic and On-call cards were cut off at some widths); muted copy rose to 6:1 and the number badges use a darker accent ink.
Here's a working CSS demo 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: Tailwind CSS Masonry Grid Dashboard Layout (Columns vs Grid Dense)
Source: https://codefronts.com/layouts/tailwind-css-dashboard-layouts/tailwind-css-masonry-grid-dashboard-layout/
Eight numbered widgets, two packing engines, one switch. Engine A is columns-[3_15rem] + break-inside-avoid: true masonry, but it reads down each column. Engine B is grid-flow-dense auto-rows-2 with row-span-(--rows): left-to-right order with holes backfilled, heights quantised to an 8px beat. The numbers on the cards make the trade-off obvious in two seconds; the switch is a radio pair read by group-has-[#tdl15-grid:checked]/board:.
## HTML
```html
<section class="tdl-15 w-full min-h-svh bg-tdl15-bg bg-linear-175 from-tdl15-bg to-tdl15-bg2 font-tdl15-sans text-tdl15-ink antialiased" aria-label="Masonry packing comparison demo">
<div class="group/board mx-auto w-full max-w-6xl p-4 sm:p-6">
<header class="mb-4 flex flex-wrap items-center gap-3.5">
<h2 class="mr-auto text-lg font-semibold tracking-tight">Widget board</h2>
<fieldset class="flex gap-1 rounded-xl border border-tdl15-edge bg-tdl15-panel p-0.75 [&>label]:flex [&>label]:h-9 [&>label]:cursor-pointer [&>label]:items-center [&>label]:gap-2 [&>label]:rounded-lg [&>label]:px-3.25 [&>label]:text-xs [&>label]:font-semibold [&>label]:text-tdl15-mut [&>label]:transition [&>label:hover]:text-tdl15-ink [&>label:has(:checked)]:bg-tdl15-acc [&>label:has(:checked)]:text-white [&>label:has(:focus-visible)]:outline-2 [&>label:has(:focus-visible)]:outline-offset-2 [&>label:has(:focus-visible)]:outline-tdl15-acc [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2">
<legend class="sr-only">Packing engine</legend>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-cols" checked><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4v16M12 4v16M19 4v16"/></svg>A · columns</label>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-grid"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="9" rx="1"/><rect x="13" y="4" width="7" height="5" rx="1"/><rect x="4" y="15" width="7" height="5" rx="1"/><rect x="13" y="11" width="7" height="9" rx="1"/></svg>B · grid dense</label>
</fieldset>
<p class="basis-full text-[13px] text-tdl15-mut"><span class="group-has-[#tdl15-grid:checked]/board:hidden">Reading order flows <b class="text-tdl15-accink">down</b> each column — note 1 → 2 → 3 stacking vertically.</span><span class="hidden group-has-[#tdl15-grid:checked]/board:inline">Reading order stays <b class="text-tdl15-accink">left → right</b>; holes are backfilled, heights snap to an 8px beat.</span></p>
</header>
<div class="gap-3.5 group-has-[#tdl15-cols:checked]/board:columns-[3_15rem] group-has-[#tdl15-cols:checked]/board:*:mb-3.5 group-has-[#tdl15-cols:checked]/board:*:break-inside-avoid group-has-[#tdl15-grid:checked]/board:grid group-has-[#tdl15-grid:checked]/board:auto-rows-2 group-has-[#tdl15-grid:checked]/board:grid-flow-dense group-has-[#tdl15-grid:checked]/board:grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))] group-has-[#tdl15-grid:checked]/board:*:row-span-(--rows) *:relative *:flex *:flex-col *:gap-2 *:overflow-hidden *:rounded-2xl *:border *:border-tdl15-edge *:bg-tdl15-panel *:p-4 *:transition *:hover:border-tdl15-acc/50 *:hover:shadow-[0_10px_24px_-18px_var(--color-tdl15-shadow)] [&_h3]:text-[10.5px] [&_h3]:font-semibold [&_h3]:tracking-[0.11em] [&_h3]:text-tdl15-mut [&_h3]:uppercase [&_.n]:absolute [&_.n]:top-2.5 [&_.n]:right-3 [&_.n]:grid [&_.n]:size-6 [&_.n]:place-items-center [&_.n]:rounded-lg [&_.n]:bg-tdl15-acc/12 [&_.n]:text-xs [&_.n]:font-bold [&_.n]:text-tdl15-accink [&_.big]:text-[26px] [&_.big]:font-semibold [&_.big]:tracking-tight [&_.big]:tabular-nums [&_.txt]:text-[13px]/[1.55] [&_.txt]:text-tdl15-mut">
<article style="--rows:6"><b class="n">1</b><h3>Sales pulse</h3><p class="big">$12,940</p><p class="txt">Today · <span class="font-semibold text-tdl15-good">▲ 7%</span> vs Friday</p></article>
<article style="--rows:9"><b class="n">2</b><h3>Traffic</h3><div aria-hidden="true" class="flex h-14 items-end gap-1.5 *:h-(--h) *:flex-1 *:rounded-t-[3px] *:bg-linear-to-t *:from-tdl15-acc/30 *:to-tdl15-acc"><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="txt">Organic up 12% after the changelog post hit the front page.</p></article>
<article style="--rows:7"><b class="n">3</b><h3>Note to self</h3><p class="txt">Ship the pricing experiment behind a flag Tuesday. Watch AOV, not conversion, for the first 48h.</p></article>
<article style="--rows:5"><b class="n">4</b><h3>NPS</h3><p class="big">+61</p><div aria-hidden="true" class="h-1.5 overflow-hidden rounded-full bg-tdl15-track"><i class="block h-full w-[80.5%] rounded-full bg-tdl15-good"></i></div></article>
<article style="--rows:8"><b class="n">5</b><h3>On-call</h3><p class="txt">Rotation: Priya → Sam → Jo.</p><p class="txt">Quiet week — 2 pages, both auto-resolved. Postmortem for #221 due Thu.</p></article>
<article style="--rows:5"><b class="n">6</b><h3>Signups</h3><p class="big">312<small class="text-[13px] font-semibold text-tdl15-mut">/wk</small></p></article>
<article style="--rows:6"><b class="n">7</b><h3>Churn watch</h3><p class="txt">3 accounts idle 21+ days. Playbook emails queued for Monday 09:00.</p></article>
<article style="--rows:6"><b class="n">8</b><h3>Build</h3><p class="big flex items-center gap-2 text-tdl15-good!"><i aria-hidden="true" class="size-2.5 rounded-full bg-tdl15-good"></i>passing</p><p class="txt">main · 4m ago</p></article>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tdl15-bg: #fbf8f4;
--color-tdl15-bg: oklch(0.978 0.007 60);
--color-tdl15-bg2: #f4efe8;
--color-tdl15-bg2: oklch(0.955 0.012 60);
--color-tdl15-panel: #ffffff;
--color-tdl15-panel: oklch(1 0 0);
--color-tdl15-track: #efe9e2;
--color-tdl15-track: oklch(0.94 0.01 60);
--color-tdl15-edge: #e8e0d6;
--color-tdl15-edge: oklch(0.91 0.015 60);
--color-tdl15-ink: #2f2620;
--color-tdl15-ink: oklch(0.27 0.02 50);
--color-tdl15-mut: #6a5d53;
--color-tdl15-mut: oklch(0.49 0.03 50);
--color-tdl15-acc: #d0612c;
--color-tdl15-acc: oklch(0.62 0.16 45);
--color-tdl15-accink: #a5461a;
--color-tdl15-accink: oklch(0.52 0.15 42);
--color-tdl15-good: #17804a;
--color-tdl15-good: oklch(0.52 0.13 155);
--color-tdl15-shadow: #7a5a44;
--color-tdl15-shadow: oklch(0.45 0.07 50 / 0.55);
--font-tdl15-sans: 'Lexend', system-ui, sans-serif;
}
.tdl-15 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tdl-15 * {
transition: none !important;
}
}
```Here's a working CSS demo 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: Tailwind CSS Masonry Grid Dashboard Layout (Columns vs Grid Dense)
Source: https://codefronts.com/layouts/tailwind-css-dashboard-layouts/tailwind-css-masonry-grid-dashboard-layout/
Eight numbered widgets, two packing engines, one switch. Engine A is columns-[3_15rem] + break-inside-avoid: true masonry, but it reads down each column. Engine B is grid-flow-dense auto-rows-2 with row-span-(--rows): left-to-right order with holes backfilled, heights quantised to an 8px beat. The numbers on the cards make the trade-off obvious in two seconds; the switch is a radio pair read by group-has-[#tdl15-grid:checked]/board:.
## HTML
```html
<section class="tdl-15 w-full min-h-svh bg-tdl15-bg bg-linear-175 from-tdl15-bg to-tdl15-bg2 font-tdl15-sans text-tdl15-ink antialiased" aria-label="Masonry packing comparison demo">
<div class="group/board mx-auto w-full max-w-6xl p-4 sm:p-6">
<header class="mb-4 flex flex-wrap items-center gap-3.5">
<h2 class="mr-auto text-lg font-semibold tracking-tight">Widget board</h2>
<fieldset class="flex gap-1 rounded-xl border border-tdl15-edge bg-tdl15-panel p-0.75 [&>label]:flex [&>label]:h-9 [&>label]:cursor-pointer [&>label]:items-center [&>label]:gap-2 [&>label]:rounded-lg [&>label]:px-3.25 [&>label]:text-xs [&>label]:font-semibold [&>label]:text-tdl15-mut [&>label]:transition [&>label:hover]:text-tdl15-ink [&>label:has(:checked)]:bg-tdl15-acc [&>label:has(:checked)]:text-white [&>label:has(:focus-visible)]:outline-2 [&>label:has(:focus-visible)]:outline-offset-2 [&>label:has(:focus-visible)]:outline-tdl15-acc [&_svg]:size-3.5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-2">
<legend class="sr-only">Packing engine</legend>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-cols" checked><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4v16M12 4v16M19 4v16"/></svg>A · columns</label>
<label><input class="sr-only" type="radio" name="tdl15" id="tdl15-grid"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="9" rx="1"/><rect x="13" y="4" width="7" height="5" rx="1"/><rect x="4" y="15" width="7" height="5" rx="1"/><rect x="13" y="11" width="7" height="9" rx="1"/></svg>B · grid dense</label>
</fieldset>
<p class="basis-full text-[13px] text-tdl15-mut"><span class="group-has-[#tdl15-grid:checked]/board:hidden">Reading order flows <b class="text-tdl15-accink">down</b> each column — note 1 → 2 → 3 stacking vertically.</span><span class="hidden group-has-[#tdl15-grid:checked]/board:inline">Reading order stays <b class="text-tdl15-accink">left → right</b>; holes are backfilled, heights snap to an 8px beat.</span></p>
</header>
<div class="gap-3.5 group-has-[#tdl15-cols:checked]/board:columns-[3_15rem] group-has-[#tdl15-cols:checked]/board:*:mb-3.5 group-has-[#tdl15-cols:checked]/board:*:break-inside-avoid group-has-[#tdl15-grid:checked]/board:grid group-has-[#tdl15-grid:checked]/board:auto-rows-2 group-has-[#tdl15-grid:checked]/board:grid-flow-dense group-has-[#tdl15-grid:checked]/board:grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))] group-has-[#tdl15-grid:checked]/board:*:row-span-(--rows) *:relative *:flex *:flex-col *:gap-2 *:overflow-hidden *:rounded-2xl *:border *:border-tdl15-edge *:bg-tdl15-panel *:p-4 *:transition *:hover:border-tdl15-acc/50 *:hover:shadow-[0_10px_24px_-18px_var(--color-tdl15-shadow)] [&_h3]:text-[10.5px] [&_h3]:font-semibold [&_h3]:tracking-[0.11em] [&_h3]:text-tdl15-mut [&_h3]:uppercase [&_.n]:absolute [&_.n]:top-2.5 [&_.n]:right-3 [&_.n]:grid [&_.n]:size-6 [&_.n]:place-items-center [&_.n]:rounded-lg [&_.n]:bg-tdl15-acc/12 [&_.n]:text-xs [&_.n]:font-bold [&_.n]:text-tdl15-accink [&_.big]:text-[26px] [&_.big]:font-semibold [&_.big]:tracking-tight [&_.big]:tabular-nums [&_.txt]:text-[13px]/[1.55] [&_.txt]:text-tdl15-mut">
<article style="--rows:6"><b class="n">1</b><h3>Sales pulse</h3><p class="big">$12,940</p><p class="txt">Today · <span class="font-semibold text-tdl15-good">▲ 7%</span> vs Friday</p></article>
<article style="--rows:9"><b class="n">2</b><h3>Traffic</h3><div aria-hidden="true" class="flex h-14 items-end gap-1.5 *:h-(--h) *:flex-1 *:rounded-t-[3px] *:bg-linear-to-t *:from-tdl15-acc/30 *:to-tdl15-acc"><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="txt">Organic up 12% after the changelog post hit the front page.</p></article>
<article style="--rows:7"><b class="n">3</b><h3>Note to self</h3><p class="txt">Ship the pricing experiment behind a flag Tuesday. Watch AOV, not conversion, for the first 48h.</p></article>
<article style="--rows:5"><b class="n">4</b><h3>NPS</h3><p class="big">+61</p><div aria-hidden="true" class="h-1.5 overflow-hidden rounded-full bg-tdl15-track"><i class="block h-full w-[80.5%] rounded-full bg-tdl15-good"></i></div></article>
<article style="--rows:8"><b class="n">5</b><h3>On-call</h3><p class="txt">Rotation: Priya → Sam → Jo.</p><p class="txt">Quiet week — 2 pages, both auto-resolved. Postmortem for #221 due Thu.</p></article>
<article style="--rows:5"><b class="n">6</b><h3>Signups</h3><p class="big">312<small class="text-[13px] font-semibold text-tdl15-mut">/wk</small></p></article>
<article style="--rows:6"><b class="n">7</b><h3>Churn watch</h3><p class="txt">3 accounts idle 21+ days. Playbook emails queued for Monday 09:00.</p></article>
<article style="--rows:6"><b class="n">8</b><h3>Build</h3><p class="big flex items-center gap-2 text-tdl15-good!"><i aria-hidden="true" class="size-2.5 rounded-full bg-tdl15-good"></i>passing</p><p class="txt">main · 4m ago</p></article>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tdl15-bg: #fbf8f4;
--color-tdl15-bg: oklch(0.978 0.007 60);
--color-tdl15-bg2: #f4efe8;
--color-tdl15-bg2: oklch(0.955 0.012 60);
--color-tdl15-panel: #ffffff;
--color-tdl15-panel: oklch(1 0 0);
--color-tdl15-track: #efe9e2;
--color-tdl15-track: oklch(0.94 0.01 60);
--color-tdl15-edge: #e8e0d6;
--color-tdl15-edge: oklch(0.91 0.015 60);
--color-tdl15-ink: #2f2620;
--color-tdl15-ink: oklch(0.27 0.02 50);
--color-tdl15-mut: #6a5d53;
--color-tdl15-mut: oklch(0.49 0.03 50);
--color-tdl15-acc: #d0612c;
--color-tdl15-acc: oklch(0.62 0.16 45);
--color-tdl15-accink: #a5461a;
--color-tdl15-accink: oklch(0.52 0.15 42);
--color-tdl15-good: #17804a;
--color-tdl15-good: oklch(0.52 0.13 155);
--color-tdl15-shadow: #7a5a44;
--color-tdl15-shadow: oklch(0.45 0.07 50 / 0.55);
--font-tdl15-sans: 'Lexend', system-ui, sans-serif;
}
.tdl-15 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tdl-15 * {
transition: none !important;
}
}
```How this works
Engine A — columns. The board is a multi-column container; cards avoid breaking across columns. Heights are natural, gaps are perfect, but the flow is vertical: card 2 sits under card 1.
<div class="columns-[3_15rem] gap-x-3.5 *:mb-3.5 *:break-inside-avoid">Engine B — grid dense. Rows are a tiny 8px beat; each card spans as many as its --rows asks for, and grid-flow-dense backfills holes, so reading order stays roughly left-to-right.
<div class="grid grid-flow-dense auto-rows-2 gap-3.5
grid-cols-[repeat(auto-fill,minmax(min(15rem,100%),1fr))]
*:row-span-(--rows)">Switching engines. Both engines' utilities live on the board behind group-has-[#tdl15-cols:checked]/board: and group-has-[#tdl15-grid:checked]/board:. Native CSS masonry (display:grid-lanes) will eventually make this choice unnecessary; until it ships everywhere, pick the trade-off that matches your content.
Make it yours
- Column width:
15remin both engines. - Card height in Engine B: each card's inline
--rows(rows × 8px + gaps). - Prefer reading order? Default to Engine B by moving
checkedto#tdl15-grid. - Accent:
--color-tdl15-acc.
Gotchas — read before shipping
- Multi-column order is top-to-bottom, so keyboard Tab order and visual order diverge — avoid Engine A for sequences (feeds, steps).
- Engine B quantises: content taller than its span is clipped (
overflow-hidden). Size spans generously or compute them in JS. grid-flow-densecan move a later card before an earlier one visually; DOM order (and screen-reader order) is unchanged.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Tailwind v4 floor; multi-column, dense grid flow and :has() are inside it.