Tailwind Dashboard: 16 Layout and Widget Examples

Tailwind dashboards usually ship without a stylesheet of their own. The sidebar, stat cards, tables and charts are all styled by classes in the markup, and v4 cut a full build of Tailwind Labs' Catalyst kit from 378 ms on v3.4 to 100 ms.

Below are 16 Tailwind dashboard examples you can copy, followed by a guide to templates, layout, charts and dark mode.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Full dashboard layouts

Whole screens built from one grid: a SaaS admin shell with a 16rem sidebar column from md: up, an analytics overview, an e-commerce orders screen and a bento layout made of col-span and row-span tiles.

SaaS admin shell with sidebar and header

  • md:grid-cols-[16rem_1fr]
  • aria-[current=page]:
  • @container
  • min-w-0
  • aria-expanded

The shell is one grid: unprefixed classes stack the sidebar above the content on phones, and md:grid md:grid-cols-[16rem_1fr] puts it in a fixed 16rem column next to a flexible one. min-w-0 on the content column stops long rows from widening the grid, and the main area is an @container so the stat cards switch to three columns by their own width. The only JS is the phone menu button, which flips aria-expanded and the hidden class on the nav.

View code
<div class="min-h-[560px] bg-slate-50 text-slate-900 md:grid md:grid-cols-[16rem_1fr]">
  <aside class="border-b border-slate-200 bg-white md:border-r md:border-b-0">
    <div class="flex h-16 items-center justify-between gap-3 px-4">
      <a href="#" class="flex items-center gap-2.5 rounded-md text-base font-semibold tracking-tight text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">
        <svg viewBox="0 0 32 32" class="size-8" aria-hidden="true"><rect width="32" height="32" rx="8" class="fill-violet-600"/><path d="M10 22V10h4v8h8v4z" class="fill-white"/></svg>
        Ledgerly
      </a>
      <button id="tdb-saas-toggle" type="button" aria-expanded="false" aria-controls="tdb-saas-nav" class="grid size-11 place-items-center rounded-lg text-slate-700 ring-1 ring-slate-200 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 md:hidden">
        <span class="sr-only">Menu</span>
        <svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
      </button>
    </div>
    <nav id="tdb-saas-nav" aria-label="Main" class="hidden px-3 pb-5 md:block">
      <ul class="space-y-1 text-sm font-medium">
        <li><a href="#" aria-current="page" class="flex min-h-10 items-center gap-3 rounded-lg px-3 text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><path d="M4 10.5 12 4l8 6.5V20h-5v-5H9v5H4z"/></svg>Overview</a></li>
        <li><a href="#" class="flex min-h-10 items-center gap-3 rounded-lg px-3 text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.5 3.4-5.5 6.5-5.5s5.7 2 6.5 5.5M16 4.8a3.5 3.5 0 0 1 0 6.4M18.5 14.8c1.5.8 2.6 2.6 3 5.2"/></svg>Customers<span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-700">1,284</span></a></li>
        <li><a href="#" class="flex min-h-10 items-center gap-3 rounded-lg px-3 text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4M9 12h6M9 16h6"/></svg>Invoices</a></li>
        <li><a href="#" class="flex min-h-10 items-center gap-3 rounded-lg px-3 text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M4 20V11M10 20V5M16 20v-6M21 20H3"/></svg>Reports</a></li>
        <li><a href="#" class="flex min-h-10 items-center gap-3 rounded-lg px-3 text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1"/></svg>Settings</a></li>
      </ul>
      <div class="mt-6 rounded-xl bg-slate-100 p-4">
        <p class="text-sm font-medium text-slate-900">Growth plan</p>
        <p class="mt-1 text-xs text-slate-600">7,840 of 10,000 invoices this month</p>
        <div class="mt-3 h-2 rounded-full bg-slate-200" role="progressbar" aria-label="Invoice quota used" aria-valuenow="78" aria-valuemin="0" aria-valuemax="100"><div class="h-full w-[78%] rounded-full bg-violet-600"></div></div>
      </div>
    </nav>
  </aside>
  <div class="flex min-w-0 flex-col">
    <header class="flex h-16 items-center gap-3 border-b border-slate-200 bg-white px-4 sm:px-6">
      <form role="search" class="relative min-w-0 flex-1 sm:max-w-xs">
        <label for="tdb-saas-q" class="sr-only">Search customers and invoices</label>
        <svg viewBox="0 0 24 24" class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-slate-500" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
        <input id="tdb-saas-q" type="search" placeholder="Search" class="h-10 w-full rounded-lg bg-slate-100 pr-3 pl-9 text-sm text-slate-900 placeholder:text-slate-600 focus:bg-white focus:outline-2 focus:outline-violet-600">
      </form>
      <button type="button" class="relative ml-auto grid size-10 shrink-0 place-items-center rounded-lg text-slate-600 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-violet-600">
        <span class="sr-only">Notifications, 3 unread</span>
        <svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9a6 6 0 1 1 12 0c0 6 2.5 8 2.5 8h-17S6 15 6 9M10 20.5h4"/></svg>
        <span class="absolute top-2 right-2 size-2 rounded-full bg-rose-500 ring-2 ring-white" aria-hidden="true"></span>
      </button>
      <svg viewBox="0 0 40 40" class="size-9 shrink-0" role="img" aria-label="Signed in as Priya Natarajan"><circle cx="20" cy="20" r="20" class="fill-amber-200"/><text x="20" y="25" text-anchor="middle" class="fill-amber-900 text-sm font-semibold">PN</text></svg>
    </header>
    <main class="@container flex-1 p-4 sm:p-6">
      <div class="flex flex-wrap items-end justify-between gap-3">
        <div>
          <h1 class="text-xl font-semibold tracking-tight text-slate-900">Overview</h1>
          <p class="text-sm text-slate-600">Tuesday, 27 October 2026</p>
        </div>
        <a href="#" class="inline-flex min-h-10 items-center gap-2 rounded-lg bg-violet-600 px-4 text-sm font-semibold text-white hover:bg-violet-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600"><svg viewBox="0 0 20 20" class="size-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M10 4v12M4 10h12"/></svg>New invoice</a>
      </div>
      <dl class="mt-5 grid gap-3 @md:grid-cols-3">
        <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200">
          <dt class="text-sm text-slate-600">Outstanding</dt>
          <dd class="mt-1 text-2xl font-semibold tabular-nums">$48,210</dd>
          <dd class="text-xs text-slate-600">23 open invoices</dd>
        </div>
        <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200">
          <dt class="text-sm text-slate-600">Paid this month</dt>
          <dd class="mt-1 text-2xl font-semibold tabular-nums">$126,904</dd>
          <dd class="text-xs text-emerald-700">Up 12% on September</dd>
        </div>
        <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200">
          <dt class="text-sm text-slate-600">Overdue</dt>
          <dd class="mt-1 text-2xl font-semibold text-rose-700 tabular-nums">$6,380</dd>
          <dd class="text-xs text-slate-600">4 invoices past 30 days</dd>
        </div>
      </dl>
      <section aria-labelledby="tdb-saas-recent" class="mt-5 rounded-xl bg-white ring-1 ring-slate-200">
        <div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
          <h2 id="tdb-saas-recent" class="text-sm font-semibold text-slate-900">Recent invoices</h2>
          <a href="#" class="rounded text-sm font-medium text-violet-700 hover:underline focus-visible:outline-2 focus-visible:outline-violet-600">View all</a>
        </div>
        <ul class="divide-y divide-slate-200 text-sm">
          <li class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
            <div class="min-w-0 flex-1"><p class="truncate font-medium text-slate-900">Northwind Joinery</p><p class="text-slate-600">INV-2093, due 2 Nov</p></div>
            <span class="rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-800 ring-1 ring-emerald-600/20 ring-inset">Paid</span>
            <p class="w-24 text-right font-medium tabular-nums">$4,200.00</p>
          </li>
          <li class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
            <div class="min-w-0 flex-1"><p class="truncate font-medium text-slate-900">Kestrel Dental Group</p><p class="text-slate-600">INV-2092, due 30 Oct</p></div>
            <span class="rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800 ring-1 ring-amber-600/20 ring-inset">Sent</span>
            <p class="w-24 text-right font-medium tabular-nums">$1,860.00</p>
          </li>
          <li class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
            <div class="min-w-0 flex-1"><p class="truncate font-medium text-slate-900">Marlow &amp; Finch</p><p class="text-slate-600">INV-2087, due 21 Sep</p></div>
            <span class="rounded-full bg-rose-50 px-2 py-0.5 text-xs font-medium text-rose-800 ring-1 ring-rose-600/20 ring-inset">Overdue</span>
            <p class="w-24 text-right font-medium tabular-nums">$2,975.50</p>
          </li>
        </ul>
      </section>
    </main>
  </div>
</div>

Analytics overview with sparklines and an area chart

  • @container
  • @3xl:grid-cols-4
  • fill-sky-500/15
  • vector-effect
  • w-[67%]

The whole screen is an @container, so the KPI row goes from one to two to four columns by its own width with @md: and @3xl:, and the chart sits beside the top pages list from @3xl: up. The sparklines and the area chart are inline SVG colored with fill-* and stroke-* utilities; preserveAspectRatio="none" stretches them to any width and vector-effect="non-scaling-stroke" keeps the lines 2px. Axis labels live in HTML so they never shrink with the SVG, and the chart has a title and desc that summarise the trend in words.

View code
<div class="@container min-h-[600px] bg-zinc-50 p-4 text-zinc-900 sm:p-6">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <div>
      <h2 class="text-lg font-semibold tracking-tight text-zinc-900">Site analytics</h2>
      <p class="text-sm text-zinc-600">fernhill-coffee.example, 1 to 28 October 2026</p>
    </div>
    <p class="inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-xs font-medium text-zinc-700 ring-1 ring-zinc-200"><span class="size-2 rounded-full bg-emerald-500" aria-hidden="true"></span>42 visitors right now</p>
  </div>
  <dl class="mt-5 grid grid-cols-1 gap-3 @md:grid-cols-2 @3xl:grid-cols-4">
    <div class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">
      <dt class="text-sm text-zinc-600">Visitors</dt>
      <dd class="mt-1 flex items-baseline justify-between gap-2"><span class="text-2xl font-semibold tabular-nums">18,402</span><span class="inline-flex items-center gap-0.5 rounded-md bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-800"><svg viewBox="0 0 12 12" class="size-3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M6 10V2M2.5 5.5 6 2l3.5 3.5"/></svg><span class="sr-only">Up</span>12.4%</span></dd>
      <dd class="mt-3"><svg viewBox="0 0 120 32" preserveAspectRatio="none" class="h-8 w-full overflow-visible" aria-hidden="true"><polyline points="0,24 9,22 18,25 28,19 37,20 46,16 55,18 65,13 74,15 83,10 92,12 102,8 111,9 120,4" class="fill-none stroke-sky-600" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg></dd>
    </div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">
      <dt class="text-sm text-zinc-600">Pageviews</dt>
      <dd class="mt-1 flex items-baseline justify-between gap-2"><span class="text-2xl font-semibold tabular-nums">52,930</span><span class="inline-flex items-center gap-0.5 rounded-md bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-800"><svg viewBox="0 0 12 12" class="size-3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M6 10V2M2.5 5.5 6 2l3.5 3.5"/></svg><span class="sr-only">Up</span>8.1%</span></dd>
      <dd class="mt-3"><svg viewBox="0 0 120 32" preserveAspectRatio="none" class="h-8 w-full overflow-visible" aria-hidden="true"><polyline points="0,20 9,23 18,18 28,20 37,15 46,17 55,14 65,16 74,11 83,13 92,9 102,11 111,7 120,8" class="fill-none stroke-sky-600" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg></dd>
    </div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">
      <dt class="text-sm text-zinc-600">Bounce rate</dt>
      <dd class="mt-1 flex items-baseline justify-between gap-2"><span class="text-2xl font-semibold tabular-nums">41.2%</span><span class="inline-flex items-center gap-0.5 rounded-md bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-800"><svg viewBox="0 0 12 12" class="size-3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M6 2v8M2.5 6.5 6 10l3.5-3.5"/></svg><span class="sr-only">Down, an improvement,</span>3.0%</span></dd>
      <dd class="mt-3"><svg viewBox="0 0 120 32" preserveAspectRatio="none" class="h-8 w-full overflow-visible" aria-hidden="true"><polyline points="0,8 9,10 18,9 28,12 37,11 46,14 55,13 65,16 74,15 83,18 92,17 102,20 111,19 120,22" class="fill-none stroke-sky-600" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg></dd>
    </div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">
      <dt class="text-sm text-zinc-600">Avg. visit</dt>
      <dd class="mt-1 flex items-baseline justify-between gap-2"><span class="text-2xl font-semibold tabular-nums">2m 48s</span><span class="inline-flex items-center gap-0.5 rounded-md bg-rose-50 px-1.5 py-0.5 text-xs font-medium text-rose-800"><svg viewBox="0 0 12 12" class="size-3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M6 2v8M2.5 6.5 6 10l3.5-3.5"/></svg><span class="sr-only">Down</span>4.6%</span></dd>
      <dd class="mt-3"><svg viewBox="0 0 120 32" preserveAspectRatio="none" class="h-8 w-full overflow-visible" aria-hidden="true"><polyline points="0,10 9,12 18,9 28,14 37,13 46,12 55,16 65,15 74,18 83,16 92,19 102,18 111,21 120,20" class="fill-none stroke-rose-600" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg></dd>
    </div>
  </dl>
  <div class="mt-4 grid gap-4 @3xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
    <figure class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">
      <figcaption class="flex flex-wrap items-baseline justify-between gap-2"><span class="text-sm font-semibold text-zinc-900">Daily visitors</span><span class="text-xs text-zinc-600">Peak 1,104 on 24 Oct</span></figcaption>
      <div class="mt-4 grid grid-cols-[auto_minmax(0,1fr)] gap-x-2">
        <div class="flex h-44 flex-col justify-between text-right text-xs text-zinc-600 tabular-nums" aria-hidden="true"><span>1.2k</span><span>800</span><span>400</span><span>0</span></div>
        <svg viewBox="0 0 280 120" preserveAspectRatio="none" class="h-44 w-full" role="img" aria-labelledby="tdb-an-title tdb-an-desc">
          <title id="tdb-an-title">Daily visitors, 1 to 28 October 2026</title>
          <desc id="tdb-an-desc">Visitors rose from about 420 a day in the first week to about 900 in the last week, with a peak of 1,104 on 24 October.</desc>
          <g class="stroke-zinc-200" stroke-width="1"><line x1="0" y1="0.5" x2="280" y2="0.5" vector-effect="non-scaling-stroke"/><line x1="0" y1="40" x2="280" y2="40" vector-effect="non-scaling-stroke"/><line x1="0" y1="80" x2="280" y2="80" vector-effect="non-scaling-stroke"/><line x1="0" y1="119.5" x2="280" y2="119.5" vector-effect="non-scaling-stroke"/></g>
          <path d="M0,120 L0,78 21.5,74 43.1,80 64.6,69 86.2,64 107.7,68 129.2,56 150.8,50 172.3,54 193.8,42 215.4,34 236.9,9.6 258.5,26 280,30 280,120 Z" class="fill-sky-500/15"/>
          <path d="M0,78 21.5,74 43.1,80 64.6,69 86.2,64 107.7,68 129.2,56 150.8,50 172.3,54 193.8,42 215.4,34 236.9,9.6 258.5,26 280,30" class="fill-none stroke-sky-600" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>
        </svg>
        <div></div>
        <div class="mt-2 flex justify-between text-xs text-zinc-600" aria-hidden="true"><span>1 Oct</span><span>8 Oct</span><span>15 Oct</span><span>22 Oct</span><span>28 Oct</span></div>
      </div>
    </figure>
    <section aria-labelledby="tdb-an-pages" class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">
      <div class="flex items-baseline justify-between"><h3 id="tdb-an-pages" class="text-sm font-semibold text-zinc-900">Top pages</h3><span class="text-xs text-zinc-600">Visitors</span></div>
      <ol class="mt-3 space-y-1.5 text-sm">
        <li class="relative flex items-center justify-between gap-3 overflow-hidden rounded-md px-2 py-1.5"><span class="absolute inset-y-0 left-0 w-full rounded-md bg-sky-100" aria-hidden="true"></span><span class="relative truncate">/</span><span class="relative font-medium tabular-nums">6,210</span></li>
        <li class="relative flex items-center justify-between gap-3 overflow-hidden rounded-md px-2 py-1.5"><span class="absolute inset-y-0 left-0 w-[67%] rounded-md bg-sky-100" aria-hidden="true"></span><span class="relative truncate">/menu</span><span class="relative font-medium tabular-nums">4,180</span></li>
        <li class="relative flex items-center justify-between gap-3 overflow-hidden rounded-md px-2 py-1.5"><span class="absolute inset-y-0 left-0 w-[48%] rounded-md bg-sky-100" aria-hidden="true"></span><span class="relative truncate">/blog/cold-brew-ratio</span><span class="relative font-medium tabular-nums">2,950</span></li>
        <li class="relative flex items-center justify-between gap-3 overflow-hidden rounded-md px-2 py-1.5"><span class="absolute inset-y-0 left-0 w-[36%] rounded-md bg-sky-100" aria-hidden="true"></span><span class="relative truncate">/shop/beans</span><span class="relative font-medium tabular-nums">2,240</span></li>
        <li class="relative flex items-center justify-between gap-3 overflow-hidden rounded-md px-2 py-1.5"><span class="absolute inset-y-0 left-0 w-[21%] rounded-md bg-sky-100" aria-hidden="true"></span><span class="relative truncate">/locations</span><span class="relative font-medium tabular-nums">1,310</span></li>
        <li class="relative flex items-center justify-between gap-3 overflow-hidden rounded-md px-2 py-1.5"><span class="absolute inset-y-0 left-0 w-[14%] rounded-md bg-sky-100" aria-hidden="true"></span><span class="relative truncate">/about</span><span class="relative font-medium tabular-nums">860</span></li>
      </ol>
    </section>
  </div>
</div>

E-commerce orders dashboard

  • aria-pressed:
  • grid-cols-2
  • @3xl:grid-cols-[minmax(0,1fr)_16rem]
  • divide-y
  • tabular-nums

Four KPI tiles sit in two columns on phones and four from @2xl:, and the orders list shares a row with a channel breakdown from @3xl: up. The filter is a group of buttons whose selected look comes entirely from aria-pressed:bg-white and aria-pressed:shadow-xs, so the script only sets aria-pressed and the hidden attribute on rows, which Preflight turns into display: none even on flex items. The channel bars are plain divs with arbitrary widths such as w-[63%], and every bar prints its dollar value.

View code
<div class="@container min-h-[620px] bg-stone-100 p-4 text-stone-900 sm:p-6">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <div>
      <h2 class="text-lg font-semibold tracking-tight text-stone-900">Orders</h2>
      <p class="text-sm text-stone-600">Copper &amp; Clay ceramics, today so far</p>
    </div>
    <button type="button" class="inline-flex min-h-10 items-center gap-2 rounded-lg bg-white px-3.5 text-sm font-medium text-stone-800 ring-1 ring-stone-300 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700"><svg viewBox="0 0 20 20" class="size-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3v10M6 9l4 4 4-4M4 16h12"/></svg>Export CSV</button>
  </div>
  <dl class="mt-5 grid grid-cols-2 gap-3 @2xl:grid-cols-4">
    <div class="rounded-xl bg-white p-4 ring-1 ring-stone-200"><dt class="text-xs font-medium text-stone-600">Orders</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">184</dd></div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-stone-200"><dt class="text-xs font-medium text-stone-600">Revenue</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">$12,480</dd></div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-stone-200"><dt class="text-xs font-medium text-stone-600">Avg. order</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">$67.83</dd></div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-stone-200"><dt class="text-xs font-medium text-stone-600">Return rate</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">2.1%</dd></div>
  </dl>
  <div class="mt-4 grid gap-4 @3xl:grid-cols-[minmax(0,1fr)_16rem]">
    <section aria-labelledby="tdb-ord-title" class="rounded-xl bg-white ring-1 ring-stone-200">
      <div class="flex flex-wrap items-center justify-between gap-3 border-b border-stone-200 p-3 pl-4">
        <h3 id="tdb-ord-title" class="text-sm font-semibold text-stone-900">Latest orders</h3>
        <div class="flex rounded-lg bg-stone-100 p-1 text-sm" role="group" aria-label="Filter orders">
          <button type="button" data-filter="all" aria-pressed="true" class="tdb-ord-filter min-h-9 rounded-md px-3 font-medium text-stone-700 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-700 aria-pressed:bg-white aria-pressed:text-stone-900 aria-pressed:shadow-xs">All</button>
          <button type="button" data-filter="unfulfilled" aria-pressed="false" class="tdb-ord-filter min-h-9 rounded-md px-3 font-medium text-stone-700 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-700 aria-pressed:bg-white aria-pressed:text-stone-900 aria-pressed:shadow-xs">Unfulfilled</button>
          <button type="button" data-filter="shipped" aria-pressed="false" class="tdb-ord-filter min-h-9 rounded-md px-3 font-medium text-stone-700 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-700 aria-pressed:bg-white aria-pressed:text-stone-900 aria-pressed:shadow-xs">Shipped</button>
        </div>
      </div>
      <ul id="tdb-ord-list" class="divide-y divide-stone-200 text-sm">
        <li data-status="unfulfilled" class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
          <div class="min-w-0 flex-1"><p class="font-medium text-stone-900">#CC-5821 <span class="font-normal text-stone-600">Marta Oyelaran</span></p><p class="truncate text-stone-600">Speckled mug set, 2 items</p></div>
          <span class="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-900">Unfulfilled</span>
          <p class="w-20 text-right font-medium tabular-nums">$96.00</p>
        </li>
        <li data-status="shipped" class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
          <div class="min-w-0 flex-1"><p class="font-medium text-stone-900">#CC-5820 <span class="font-normal text-stone-600">Leo Brandt</span></p><p class="truncate text-stone-600">Ash glaze serving bowl</p></div>
          <span class="rounded-full bg-sky-100 px-2 py-0.5 text-xs font-medium text-sky-900">Shipped</span>
          <p class="w-20 text-right font-medium tabular-nums">$142.00</p>
        </li>
        <li data-status="unfulfilled" class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
          <div class="min-w-0 flex-1"><p class="font-medium text-stone-900">#CC-5819 <span class="font-normal text-stone-600">Hana Sato</span></p><p class="truncate text-stone-600">Bud vase trio, gift wrap</p></div>
          <span class="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-900">Unfulfilled</span>
          <p class="w-20 text-right font-medium tabular-nums">$58.50</p>
        </li>
        <li data-status="shipped" class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
          <div class="min-w-0 flex-1"><p class="font-medium text-stone-900">#CC-5818 <span class="font-normal text-stone-600">Dario Russo</span></p><p class="truncate text-stone-600">Dinner plates, set of 6</p></div>
          <span class="rounded-full bg-sky-100 px-2 py-0.5 text-xs font-medium text-sky-900">Shipped</span>
          <p class="w-20 text-right font-medium tabular-nums">$210.00</p>
        </li>
        <li data-status="refunded" class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
          <div class="min-w-0 flex-1"><p class="font-medium text-stone-900">#CC-5817 <span class="font-normal text-stone-600">Amara Eze</span></p><p class="truncate text-stone-600">Planter, arrived chipped</p></div>
          <span class="rounded-full bg-stone-200 px-2 py-0.5 text-xs font-medium text-stone-800">Refunded</span>
          <p class="w-20 text-right font-medium text-stone-600 tabular-nums line-through">$44.00</p>
        </li>
        <li data-status="shipped" class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3">
          <div class="min-w-0 flex-1"><p class="font-medium text-stone-900">#CC-5816 <span class="font-normal text-stone-600">Nils Ekstrรถm</span></p><p class="truncate text-stone-600">Espresso cups, set of 4</p></div>
          <span class="rounded-full bg-sky-100 px-2 py-0.5 text-xs font-medium text-sky-900">Shipped</span>
          <p class="w-20 text-right font-medium tabular-nums">$64.00</p>
        </li>
      </ul>
      <p id="tdb-ord-status" class="border-t border-stone-200 px-4 py-2.5 text-xs text-stone-600" aria-live="polite">Showing 6 of 6 orders</p>
    </section>
    <section aria-labelledby="tdb-ord-ch" class="rounded-xl bg-white p-4 ring-1 ring-stone-200">
      <h3 id="tdb-ord-ch" class="text-sm font-semibold text-stone-900">Sales by channel</h3>
      <ul class="mt-4 space-y-4 text-sm">
        <li><div class="flex justify-between gap-2"><span>Online store</span><span class="font-medium tabular-nums">$7,920</span></div><div class="mt-1.5 h-2 rounded-full bg-stone-100"><div class="h-full w-[63%] rounded-full bg-orange-700"></div></div></li>
        <li><div class="flex justify-between gap-2"><span>Weekend markets</span><span class="font-medium tabular-nums">$2,460</span></div><div class="mt-1.5 h-2 rounded-full bg-stone-100"><div class="h-full w-[20%] rounded-full bg-orange-700"></div></div></li>
        <li><div class="flex justify-between gap-2"><span>Wholesale</span><span class="font-medium tabular-nums">$1,600</span></div><div class="mt-1.5 h-2 rounded-full bg-stone-100"><div class="h-full w-[13%] rounded-full bg-orange-700"></div></div></li>
        <li><div class="flex justify-between gap-2"><span>Social shop</span><span class="font-medium tabular-nums">$500</span></div><div class="mt-1.5 h-2 rounded-full bg-stone-100"><div class="h-full w-[4%] rounded-full bg-orange-700"></div></div></li>
      </ul>
      <p class="mt-5 rounded-lg bg-orange-50 p-3 text-xs text-orange-900">2 orders are waiting more than 24 hours for fulfilment.</p>
    </section>
  </div>
</div>

Bento grid of widgets

  • col-span-2
  • row-span-2
  • auto-rows-[minmax(7.5rem,auto)]
  • md:grid-cols-4
  • stroke-dasharray

A two-column grid on phones becomes four columns from md:, and auto-rows-[minmax(7.5rem,auto)] gives every row the same base height so col-span-2 and row-span-2 tiles lock together like a bento box. The source order is picked so automatic placement fills the holes at both sizes without grid-flow-dense. The revenue bars are divs with h-[..%] heights inside a flex items-end row, and the goal ring is an SVG circle whose stroke-dasharray shows 72 percent.

View code
<div class="w-full max-w-4xl">
  <div class="grid auto-rows-[minmax(7.5rem,auto)] grid-cols-2 gap-3 md:grid-cols-4">
    <section aria-labelledby="tdb-bn-mrr" class="col-span-2 row-span-2 flex flex-col rounded-2xl bg-indigo-950 p-5 text-white">
      <div class="flex flex-wrap items-start justify-between gap-2">
        <div>
          <h2 id="tdb-bn-mrr" class="text-sm font-medium text-indigo-200">Monthly recurring revenue</h2>
          <p class="mt-1 text-3xl font-semibold tracking-tight tabular-nums">$84,320</p>
        </div>
        <span class="rounded-full bg-white/10 px-2.5 py-1 text-xs font-medium text-lime-300">+6.8% this month</span>
      </div>
      <div class="mt-auto pt-5">
        <div class="flex h-28 items-end gap-1.5" role="img" aria-label="MRR for the last 12 months, rising from $52,100 in November 2025 to $84,320 in October 2026">
          <div class="h-[62%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[64%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[63%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[68%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[71%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[74%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[73%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[79%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[84%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[88%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-[93%] flex-1 rounded-t bg-indigo-400/60"></div>
          <div class="h-full flex-1 rounded-t bg-lime-300"></div>
        </div>
        <div class="mt-2 flex justify-between text-xs text-indigo-200" aria-hidden="true"><span>Nov 2025</span><span>Oct 2026</span></div>
      </div>
    </section>
    <section aria-labelledby="tdb-bn-users" class="rounded-2xl bg-white p-4 ring-1 ring-slate-200">
      <h2 id="tdb-bn-users" class="text-sm font-medium text-slate-600">Active users</h2>
      <p class="mt-1 text-2xl font-semibold text-slate-900 tabular-nums">3,912</p>
      <p class="mt-1 text-xs text-emerald-700">+214 this week</p>
    </section>
    <section aria-labelledby="tdb-bn-goal" class="row-span-2 flex flex-col rounded-2xl bg-white p-4 ring-1 ring-slate-200">
      <h2 id="tdb-bn-goal" class="text-sm font-medium text-slate-600">Q4 revenue goal</h2>
      <div class="relative mx-auto my-auto w-full max-w-32 py-3">
        <svg viewBox="0 0 100 100" class="w-full -rotate-90" role="img" aria-label="72 percent of the Q4 goal reached"><circle cx="50" cy="50" r="40" fill="none" class="stroke-slate-100" stroke-width="12"/><circle cx="50" cy="50" r="40" fill="none" class="stroke-indigo-600" stroke-width="12" stroke-linecap="round" stroke-dasharray="181 252"/></svg>
        <p class="absolute inset-0 grid place-items-center text-xl font-semibold text-slate-900 tabular-nums" aria-hidden="true">72%</p>
      </div>
      <p class="text-xs text-slate-600">$181k of $252k, 64 days left</p>
    </section>
    <section aria-labelledby="tdb-bn-churn" class="rounded-2xl bg-lime-200 p-4 text-lime-950">
      <h2 id="tdb-bn-churn" class="text-sm font-medium">Churn</h2>
      <p class="mt-1 text-2xl font-semibold tabular-nums">1.9%</p>
      <p class="mt-1 text-xs">Down from 2.4% in September</p>
    </section>
    <section aria-labelledby="tdb-bn-payout" class="col-span-2 flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-amber-100 p-4 text-amber-950">
      <div>
        <h2 id="tdb-bn-payout" class="text-sm font-medium">Next payout</h2>
        <p class="mt-1 text-2xl font-semibold tabular-nums">$21,450</p>
        <p class="text-xs">1 November, to the account ending 0217</p>
      </div>
      <a href="#" class="inline-flex min-h-10 items-center rounded-lg bg-amber-950 px-4 text-sm font-semibold text-amber-50 hover:bg-amber-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-950">Details</a>
    </section>
    <section aria-labelledby="tdb-bn-nps" class="rounded-2xl bg-white p-4 ring-1 ring-slate-200">
      <h2 id="tdb-bn-nps" class="text-sm font-medium text-slate-600">NPS</h2>
      <p class="mt-1 text-2xl font-semibold text-slate-900 tabular-nums">54</p>
      <p class="mt-1 text-xs text-slate-600">312 responses</p>
    </section>
    <section aria-labelledby="tdb-bn-call" class="rounded-2xl bg-white p-4 ring-1 ring-slate-200">
      <h2 id="tdb-bn-call" class="text-sm font-medium text-slate-600">On call today</h2>
      <ul class="mt-3 flex -space-x-2">
        <li><svg viewBox="0 0 36 36" class="size-9 rounded-full ring-2 ring-white" role="img" aria-label="Rafael Ortiz"><circle cx="18" cy="18" r="18" class="fill-sky-200"/><text x="18" y="23" text-anchor="middle" class="fill-sky-900 text-xs font-semibold">RO</text></svg></li>
        <li><svg viewBox="0 0 36 36" class="size-9 rounded-full ring-2 ring-white" role="img" aria-label="Mei Lin"><circle cx="18" cy="18" r="18" class="fill-rose-200"/><text x="18" y="23" text-anchor="middle" class="fill-rose-900 text-xs font-semibold">ML</text></svg></li>
        <li><svg viewBox="0 0 36 36" class="size-9 rounded-full ring-2 ring-white" role="img" aria-label="Kwame Asante"><circle cx="18" cy="18" r="18" class="fill-emerald-200"/><text x="18" y="23" text-anchor="middle" class="fill-emerald-900 text-xs font-semibold">KA</text></svg></li>
      </ul>
      <p class="mt-2 text-xs text-slate-600">Support until 18:00</p>
    </section>
  </div>
</div>

Boards for real teams

Dashboards shaped by the job: a kanban board that scrolls sideways inside its own box, a CRM pipeline, a budget tracker with pace markers and a dark server status board.

Project board with sideways-scrolling columns

  • overflow-x-auto
  • snap-x
  • w-72 shrink-0
  • snap-start
  • focus-visible:outline

The board is a flex row with overflow-x-auto, so only the board scrolls sideways and the page never does; w-72 shrink-0 keeps every column the same width and snap-x with snap-start lines a column up after each swipe. The scroll box is a labelled region with tabindex="0", which lets keyboard users scroll it with the arrow keys and shows a focus-visible outline. Cards are list items with a label chip, a due date and a progress bar, and Done cards are muted with line-through.

View code
<div class="min-h-[540px] bg-[#f4f1ea] text-neutral-900">
  <div class="flex flex-wrap items-center justify-between gap-3 border-b border-neutral-300 px-4 py-4 sm:px-6">
    <div>
      <h2 class="text-lg font-semibold tracking-tight text-neutral-900">Website relaunch</h2>
      <p class="text-sm text-neutral-600">Sprint 14, ends Friday 6 November</p>
    </div>
    <div class="flex items-center gap-3">
      <ul class="flex -space-x-2">
        <li><svg viewBox="0 0 32 32" class="size-8 rounded-full ring-2 ring-[#f4f1ea]" role="img" aria-label="Elena Petrova"><circle cx="16" cy="16" r="16" class="fill-teal-200"/><text x="16" y="20.5" text-anchor="middle" class="fill-teal-900 text-[11px] font-semibold">EP</text></svg></li>
        <li><svg viewBox="0 0 32 32" class="size-8 rounded-full ring-2 ring-[#f4f1ea]" role="img" aria-label="Sam Whitfield"><circle cx="16" cy="16" r="16" class="fill-orange-200"/><text x="16" y="20.5" text-anchor="middle" class="fill-orange-900 text-[11px] font-semibold">SW</text></svg></li>
        <li><svg viewBox="0 0 32 32" class="size-8 rounded-full ring-2 ring-[#f4f1ea]" role="img" aria-label="Yusuf Demir"><circle cx="16" cy="16" r="16" class="fill-violet-200"/><text x="16" y="20.5" text-anchor="middle" class="fill-violet-900 text-[11px] font-semibold">YD</text></svg></li>
      </ul>
      <p class="text-sm text-neutral-700"><span class="font-semibold tabular-nums">18</span> of 31 done</p>
    </div>
  </div>
  <div id="tdb-kb-board" role="region" aria-label="Sprint board, scrolls sideways" tabindex="0" class="flex snap-x snap-mandatory scroll-px-4 gap-4 overflow-x-auto p-4 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-neutral-900 sm:scroll-px-6 sm:px-6">
    <section aria-labelledby="tdb-kb-c1" class="w-72 shrink-0 snap-start rounded-xl bg-neutral-900/5 p-3">
      <h3 id="tdb-kb-c1" class="flex items-center justify-between px-1 text-sm font-semibold text-neutral-900">Backlog <span class="rounded-full bg-white px-2 text-xs font-medium text-neutral-700">3</span></h3>
      <ul class="mt-3 space-y-2">
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Write alt text for product photos</p>
          <div class="mt-3 flex items-center justify-between gap-2"><span class="rounded bg-violet-100 px-1.5 py-0.5 text-xs font-medium text-violet-800">Content</span><span class="text-xs text-neutral-600">Due 3 Nov</span></div>
        </li>
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Redirect map for old blog URLs</p>
          <div class="mt-3 flex items-center justify-between gap-2"><span class="rounded bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-800">SEO</span><span class="text-xs text-neutral-600">Due 4 Nov</span></div>
        </li>
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Cookie banner copy review</p>
          <div class="mt-3 flex items-center justify-between gap-2"><span class="rounded bg-rose-100 px-1.5 py-0.5 text-xs font-medium text-rose-800">Legal</span><span class="text-xs text-neutral-600">No date</span></div>
        </li>
      </ul>
    </section>
    <section aria-labelledby="tdb-kb-c2" class="w-72 shrink-0 snap-start rounded-xl bg-neutral-900/5 p-3">
      <h3 id="tdb-kb-c2" class="flex items-center justify-between px-1 text-sm font-semibold text-neutral-900">In progress <span class="rounded-full bg-white px-2 text-xs font-medium text-neutral-700">3</span></h3>
      <ul class="mt-3 space-y-2">
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Build pricing page in the new grid</p>
          <div class="mt-3 flex items-center gap-2 text-xs text-neutral-600"><div class="h-1.5 flex-1 rounded-full bg-neutral-200" role="progressbar" aria-label="Checklist" aria-valuenow="3" aria-valuemin="0" aria-valuemax="5"><div class="h-full w-3/5 rounded-full bg-teal-600"></div></div><span class="tabular-nums">3/5</span></div>
          <div class="mt-3 flex items-center justify-between gap-2"><span class="rounded bg-teal-100 px-1.5 py-0.5 text-xs font-medium text-teal-800">Front end</span><svg viewBox="0 0 24 24" class="size-6 rounded-full" role="img" aria-label="Assigned to Elena Petrova"><circle cx="12" cy="12" r="12" class="fill-teal-200"/><text x="12" y="16" text-anchor="middle" class="fill-teal-900 text-[9px] font-semibold">EP</text></svg></div>
        </li>
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Product photography, autumn range</p>
          <div class="mt-3 flex items-center justify-between gap-2"><span class="rounded bg-violet-100 px-1.5 py-0.5 text-xs font-medium text-violet-800">Content</span><span class="rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-900">Due today</span></div>
        </li>
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Checkout error states</p>
          <div class="mt-3 flex items-center justify-between gap-2"><span class="rounded bg-orange-100 px-1.5 py-0.5 text-xs font-medium text-orange-900">Design</span><svg viewBox="0 0 24 24" class="size-6 rounded-full" role="img" aria-label="Assigned to Sam Whitfield"><circle cx="12" cy="12" r="12" class="fill-orange-200"/><text x="12" y="16" text-anchor="middle" class="fill-orange-900 text-[9px] font-semibold">SW</text></svg></div>
        </li>
      </ul>
    </section>
    <section aria-labelledby="tdb-kb-c3" class="w-72 shrink-0 snap-start rounded-xl bg-neutral-900/5 p-3">
      <h3 id="tdb-kb-c3" class="flex items-center justify-between px-1 text-sm font-semibold text-neutral-900">Review <span class="rounded-full bg-white px-2 text-xs font-medium text-neutral-700">2</span></h3>
      <ul class="mt-3 space-y-2">
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Navigation with mega menu</p>
          <p class="mt-1 text-xs text-neutral-600">2 comments, waiting on Yusuf</p>
          <div class="mt-3"><span class="rounded bg-teal-100 px-1.5 py-0.5 text-xs font-medium text-teal-800">Front end</span></div>
        </li>
        <li class="rounded-lg bg-white p-3 shadow-xs ring-1 ring-neutral-900/5">
          <p class="text-sm font-medium">Accessibility pass on forms</p>
          <p class="mt-1 text-xs text-neutral-600">Contrast fixes pushed</p>
          <div class="mt-3"><span class="rounded bg-sky-100 px-1.5 py-0.5 text-xs font-medium text-sky-800">QA</span></div>
        </li>
      </ul>
    </section>
    <section aria-labelledby="tdb-kb-c4" class="w-72 shrink-0 snap-start rounded-xl bg-neutral-900/5 p-3">
      <h3 id="tdb-kb-c4" class="flex items-center justify-between px-1 text-sm font-semibold text-neutral-900">Done <span class="rounded-full bg-white px-2 text-xs font-medium text-neutral-700">2</span></h3>
      <ul class="mt-3 space-y-2">
        <li class="rounded-lg bg-white/60 p-3 ring-1 ring-neutral-900/5">
          <p class="text-sm text-neutral-600 line-through">Pick the new type scale</p>
        </li>
        <li class="rounded-lg bg-white/60 p-3 ring-1 ring-neutral-900/5">
          <p class="text-sm text-neutral-600 line-through">Staging server on the new host</p>
        </li>
      </ul>
    </section>
  </div>
</div>

CRM pipeline by stage

  • @container
  • flex h-3 overflow-hidden
  • @xl:grid-cols-5
  • w-[32%]
  • role="img"

A single flex row with overflow-hidden rounded-full turns five divs with arbitrary widths into a stacked bar, and the role="img" label repeats every value in words. The stage tiles below share the same teal ramp from teal-200 to teal-900, sit in two columns until the card reaches @xl: and five after it. The deals list and the conversion bars split into two columns from @2xl:, so the card works in a wide main area and in a narrow side panel alike.

View code
<div class="@container w-full max-w-3xl rounded-2xl bg-white p-5 text-slate-900 shadow-sm ring-1 ring-slate-200">
  <div class="flex flex-wrap items-baseline justify-between gap-2">
    <h2 class="text-base font-semibold text-slate-900">Sales pipeline, Q4 2026</h2>
    <p class="text-sm text-slate-600"><span class="font-semibold text-slate-900 tabular-nums">$1.32M</span> across 48 deals</p>
  </div>
  <div class="mt-4 flex h-3 gap-0.5 overflow-hidden rounded-full" role="img" aria-label="Pipeline value by stage: Lead $420k, Qualified $310k, Proposal $260k, Negotiation $180k, Won $150k">
    <div class="w-[32%] bg-teal-200"></div>
    <div class="w-[23%] bg-teal-400"></div>
    <div class="w-[20%] bg-teal-600"></div>
    <div class="w-[14%] bg-teal-800"></div>
    <div class="w-[11%] bg-amber-400"></div>
  </div>
  <ol class="mt-4 grid grid-cols-2 gap-2 @xl:grid-cols-5">
    <li class="rounded-lg bg-slate-50 p-3"><p class="flex items-center gap-2 text-xs font-medium text-slate-600"><span class="size-2.5 rounded-sm bg-teal-200" aria-hidden="true"></span>Lead</p><p class="mt-1 text-lg font-semibold tabular-nums">$420k</p><p class="text-xs text-slate-600">18 deals</p></li>
    <li class="rounded-lg bg-slate-50 p-3"><p class="flex items-center gap-2 text-xs font-medium text-slate-600"><span class="size-2.5 rounded-sm bg-teal-400" aria-hidden="true"></span>Qualified</p><p class="mt-1 text-lg font-semibold tabular-nums">$310k</p><p class="text-xs text-slate-600">12 deals</p></li>
    <li class="rounded-lg bg-slate-50 p-3"><p class="flex items-center gap-2 text-xs font-medium text-slate-600"><span class="size-2.5 rounded-sm bg-teal-600" aria-hidden="true"></span>Proposal</p><p class="mt-1 text-lg font-semibold tabular-nums">$260k</p><p class="text-xs text-slate-600">9 deals</p></li>
    <li class="rounded-lg bg-slate-50 p-3"><p class="flex items-center gap-2 text-xs font-medium text-slate-600"><span class="size-2.5 rounded-sm bg-teal-800" aria-hidden="true"></span>Negotiation</p><p class="mt-1 text-lg font-semibold tabular-nums">$180k</p><p class="text-xs text-slate-600">5 deals</p></li>
    <li class="rounded-lg bg-amber-50 p-3"><p class="flex items-center gap-2 text-xs font-medium text-amber-900"><span class="size-2.5 rounded-sm bg-amber-400" aria-hidden="true"></span>Won</p><p class="mt-1 text-lg font-semibold tabular-nums">$150k</p><p class="text-xs text-amber-900">4 deals</p></li>
  </ol>
  <div class="mt-6 grid gap-6 @2xl:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
    <section aria-labelledby="tdb-crm-closing">
      <h3 id="tdb-crm-closing" class="text-sm font-semibold text-slate-900">Closing this month</h3>
      <ul class="mt-2 divide-y divide-slate-200 text-sm">
        <li class="flex items-center gap-3 py-2.5">
          <svg viewBox="0 0 32 32" class="size-8 shrink-0" role="img" aria-label="Owner Ines Varga"><circle cx="16" cy="16" r="16" class="fill-teal-100"/><text x="16" y="20.5" text-anchor="middle" class="fill-teal-900 text-[11px] font-semibold">IV</text></svg>
          <div class="min-w-0 flex-1"><p class="truncate font-medium">Brightwater Clinics</p><p class="text-xs text-slate-600">Negotiation, closes 12 Nov</p></div>
          <div class="text-right"><p class="font-medium tabular-nums">$64,000</p><p class="text-xs text-teal-800 tabular-nums">80% likely</p></div>
        </li>
        <li class="flex items-center gap-3 py-2.5">
          <svg viewBox="0 0 32 32" class="size-8 shrink-0" role="img" aria-label="Owner Tom Mbeki"><circle cx="16" cy="16" r="16" class="fill-amber-100"/><text x="16" y="20.5" text-anchor="middle" class="fill-amber-900 text-[11px] font-semibold">TM</text></svg>
          <div class="min-w-0 flex-1"><p class="truncate font-medium">Halvorsen Freight</p><p class="text-xs text-slate-600">Proposal, closes 18 Nov</p></div>
          <div class="text-right"><p class="font-medium tabular-nums">$38,500</p><p class="text-xs text-slate-600 tabular-nums">55% likely</p></div>
        </li>
        <li class="flex items-center gap-3 py-2.5">
          <svg viewBox="0 0 32 32" class="size-8 shrink-0" role="img" aria-label="Owner Ines Varga"><circle cx="16" cy="16" r="16" class="fill-teal-100"/><text x="16" y="20.5" text-anchor="middle" class="fill-teal-900 text-[11px] font-semibold">IV</text></svg>
          <div class="min-w-0 flex-1"><p class="truncate font-medium">Okafor &amp; Lane</p><p class="text-xs text-slate-600">Negotiation, closes 21 Nov</p></div>
          <div class="text-right"><p class="font-medium tabular-nums">$22,000</p><p class="text-xs text-teal-800 tabular-nums">70% likely</p></div>
        </li>
        <li class="flex items-center gap-3 py-2.5">
          <svg viewBox="0 0 32 32" class="size-8 shrink-0" role="img" aria-label="Owner Tom Mbeki"><circle cx="16" cy="16" r="16" class="fill-amber-100"/><text x="16" y="20.5" text-anchor="middle" class="fill-amber-900 text-[11px] font-semibold">TM</text></svg>
          <div class="min-w-0 flex-1"><p class="truncate font-medium">Pinecrest Schools</p><p class="text-xs text-slate-600">Proposal, closes 28 Nov</p></div>
          <div class="text-right"><p class="font-medium tabular-nums">$17,800</p><p class="text-xs text-slate-600 tabular-nums">40% likely</p></div>
        </li>
      </ul>
    </section>
    <section aria-labelledby="tdb-crm-conv">
      <h3 id="tdb-crm-conv" class="text-sm font-semibold text-slate-900">Stage conversion, last 90 days</h3>
      <ul class="mt-3 space-y-3 text-sm">
        <li><div class="flex justify-between gap-2"><span class="text-slate-700">Lead to qualified</span><span class="font-medium tabular-nums">64%</span></div><div class="mt-1 h-1.5 rounded-full bg-slate-100"><div class="h-full w-[64%] rounded-full bg-teal-600"></div></div></li>
        <li><div class="flex justify-between gap-2"><span class="text-slate-700">Qualified to proposal</span><span class="font-medium tabular-nums">71%</span></div><div class="mt-1 h-1.5 rounded-full bg-slate-100"><div class="h-full w-[71%] rounded-full bg-teal-600"></div></div></li>
        <li><div class="flex justify-between gap-2"><span class="text-slate-700">Proposal to negotiation</span><span class="font-medium tabular-nums">52%</span></div><div class="mt-1 h-1.5 rounded-full bg-slate-100"><div class="h-full w-[52%] rounded-full bg-teal-600"></div></div></li>
        <li><div class="flex justify-between gap-2"><span class="text-slate-700">Negotiation to won</span><span class="font-medium tabular-nums">78%</span></div><div class="mt-1 h-1.5 rounded-full bg-slate-100"><div class="h-full w-[78%] rounded-full bg-amber-400"></div></div></li>
      </ul>
      <p class="mt-4 rounded-lg bg-slate-50 p-3 text-xs text-slate-700">Overall win rate <span class="font-semibold text-slate-900">17%</span>, median cycle 41 days.</p>
    </section>
  </div>
</div>

Budget dashboard with progress bars and a pace marker

  • role="progressbar"
  • aria-valuetext
  • left-[90%]
  • bg-rose-600
  • sm:grid-cols-3

Each category bar is a role="progressbar" div with aria-valuetext that reads the real amounts, including an over-budget line, so the color is never the only signal. A thin absolute line at left-[90%] marks how far through the month we are, which turns every bar into a pace check: anything past the line is spending ahead of schedule. Colors move from emerald to amber at 90 percent and to rose when the budget is gone.

View code
<div class="w-full max-w-2xl rounded-2xl bg-white p-5 text-stone-900 shadow-sm ring-1 ring-stone-200 sm:p-6">
  <div class="flex flex-wrap items-baseline justify-between gap-2">
    <h2 class="text-base font-semibold text-stone-900">Studio budget, October</h2>
    <p class="text-sm text-stone-600">Day 28 of 31</p>
  </div>
  <dl class="mt-4 grid grid-cols-3 gap-2 text-center sm:gap-3">
    <div class="rounded-xl bg-stone-50 p-3"><dt class="text-xs text-stone-600">Budget</dt><dd class="mt-0.5 font-semibold tabular-nums sm:text-lg">$18,000</dd></div>
    <div class="rounded-xl bg-stone-50 p-3"><dt class="text-xs text-stone-600">Spent</dt><dd class="mt-0.5 font-semibold tabular-nums sm:text-lg">$13,940</dd></div>
    <div class="rounded-xl bg-emerald-50 p-3"><dt class="text-xs text-emerald-800">Left</dt><dd class="mt-0.5 font-semibold text-emerald-900 tabular-nums sm:text-lg">$4,060</dd></div>
  </dl>
  <ul class="mt-6 space-y-4 text-sm">
    <li>
      <div class="flex flex-wrap justify-between gap-x-3"><span class="font-medium">Payroll</span><span class="text-stone-600 tabular-nums"><span class="font-medium text-stone-900">$9,200</span> of $9,500</span></div>
      <div class="relative mt-1.5 h-2.5 rounded-full bg-stone-100" role="progressbar" aria-label="Payroll" aria-valuemin="0" aria-valuemax="9500" aria-valuenow="9200" aria-valuetext="$9,200 of $9,500"><div class="h-full w-[97%] rounded-full bg-amber-500"></div><span class="absolute -inset-y-1 left-[90%] w-0.5 rounded bg-stone-900" aria-hidden="true"></span></div>
    </li>
    <li>
      <div class="flex flex-wrap justify-between gap-x-3"><span class="font-medium">Software</span><span class="tabular-nums"><span class="font-medium text-rose-700">Over by $180</span><span class="text-stone-600">, $1,180 of $1,000</span></span></div>
      <div class="relative mt-1.5 h-2.5 rounded-full bg-stone-100" role="progressbar" aria-label="Software" aria-valuemin="0" aria-valuemax="1000" aria-valuenow="1000" aria-valuetext="$1,180 of $1,000, over budget by $180"><div class="h-full w-full rounded-full bg-rose-600"></div><span class="absolute -inset-y-1 left-[90%] w-0.5 rounded bg-stone-900" aria-hidden="true"></span></div>
    </li>
    <li>
      <div class="flex flex-wrap justify-between gap-x-3"><span class="font-medium">Rent</span><span class="text-stone-600 tabular-nums"><span class="font-medium text-stone-900">$2,400</span> of $2,400</span></div>
      <div class="relative mt-1.5 h-2.5 rounded-full bg-stone-100" role="progressbar" aria-label="Rent" aria-valuemin="0" aria-valuemax="2400" aria-valuenow="2400" aria-valuetext="$2,400 of $2,400, paid in full"><div class="h-full w-full rounded-full bg-stone-400"></div><span class="absolute -inset-y-1 left-[90%] w-0.5 rounded bg-stone-900" aria-hidden="true"></span></div>
    </li>
    <li>
      <div class="flex flex-wrap justify-between gap-x-3"><span class="font-medium">Travel</span><span class="text-stone-600 tabular-nums"><span class="font-medium text-stone-900">$640</span> of $1,800</span></div>
      <div class="relative mt-1.5 h-2.5 rounded-full bg-stone-100" role="progressbar" aria-label="Travel" aria-valuemin="0" aria-valuemax="1800" aria-valuenow="640" aria-valuetext="$640 of $1,800"><div class="h-full w-[36%] rounded-full bg-emerald-600"></div><span class="absolute -inset-y-1 left-[90%] w-0.5 rounded bg-stone-900" aria-hidden="true"></span></div>
    </li>
    <li>
      <div class="flex flex-wrap justify-between gap-x-3"><span class="font-medium">Marketing</span><span class="text-stone-600 tabular-nums"><span class="font-medium text-stone-900">$520</span> of $2,000</span></div>
      <div class="relative mt-1.5 h-2.5 rounded-full bg-stone-100" role="progressbar" aria-label="Marketing" aria-valuemin="0" aria-valuemax="2000" aria-valuenow="520" aria-valuetext="$520 of $2,000"><div class="h-full w-[26%] rounded-full bg-emerald-600"></div><span class="absolute -inset-y-1 left-[90%] w-0.5 rounded bg-stone-900" aria-hidden="true"></span></div>
    </li>
    <li>
      <div class="flex flex-wrap justify-between gap-x-3"><span class="font-medium">Equipment</span><span class="text-stone-600 tabular-nums"><span class="font-medium text-stone-900">$0</span> of $1,300</span></div>
      <div class="relative mt-1.5 h-2.5 rounded-full bg-stone-100" role="progressbar" aria-label="Equipment" aria-valuemin="0" aria-valuemax="1300" aria-valuenow="0" aria-valuetext="Nothing spent of $1,300"><span class="absolute -inset-y-1 left-[90%] w-0.5 rounded bg-stone-900" aria-hidden="true"></span></div>
    </li>
  </ul>
  <div class="mt-6 flex flex-wrap items-center gap-x-5 gap-y-2 border-t border-stone-200 pt-4 text-xs text-stone-600">
    <span class="inline-flex items-center gap-1.5"><span class="h-3 w-0.5 rounded bg-stone-900" aria-hidden="true"></span>Today, 90% through the month</span>
    <span class="inline-flex items-center gap-1.5"><span class="size-2.5 rounded-full bg-emerald-600" aria-hidden="true"></span>On track</span>
    <span class="inline-flex items-center gap-1.5"><span class="size-2.5 rounded-full bg-amber-500" aria-hidden="true"></span>Above 90%</span>
    <span class="inline-flex items-center gap-1.5"><span class="size-2.5 rounded-full bg-rose-600" aria-hidden="true"></span>Over budget</span>
  </div>
</div>

Server status board on a dark stage

  • bg-[#0f141f]
  • font-mono
  • @2xl:grid-cols-[13rem_1fr_4.5rem]
  • motion-safe:animate-ping
  • createElement

Every service row is a small grid that stacks on phones and becomes name, uptime strip and percentage from @2xl:. The 30-day strips start as empty flex rows with a data-days string and a role="img" summary; a few lines of JS create one span per day and pick bg-emerald-500, bg-amber-400 or bg-rose-500, written out in full so Tailwind finds them. The live dot pings only inside motion-safe:, and the node meters switch to amber above 75 percent.

View code
<div class="@container w-full max-w-4xl rounded-2xl bg-[#0f141f] p-5 text-slate-200 ring-1 ring-white/10">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <div>
      <h2 class="text-base font-semibold text-white">Production status</h2>
      <p class="font-mono text-xs text-slate-400">eu-west-2, updated 14:02 UTC</p>
    </div>
    <p class="inline-flex items-center gap-2 rounded-full bg-amber-400/10 px-3 py-1.5 text-xs font-medium text-amber-300 ring-1 ring-amber-400/30">
      <span class="relative flex size-2"><span class="absolute inline-flex size-full rounded-full bg-amber-400 opacity-75 motion-safe:animate-ping"></span><span class="relative inline-flex size-2 rounded-full bg-amber-400"></span></span>
      1 service degraded
    </p>
  </div>
  <ul class="mt-5 divide-y divide-white/10 rounded-xl bg-white/[0.03] ring-1 ring-white/10">
    <li class="grid items-center gap-2 px-4 py-3 @2xl:grid-cols-[13rem_1fr_4.5rem] @2xl:gap-4">
      <p class="flex items-center gap-2 text-sm"><span class="size-2 rounded-full bg-emerald-400" aria-hidden="true"></span><span class="font-medium text-white">API gateway</span><span class="text-xs text-slate-400">Operational</span></p>
      <div class="tdb-ops-strip flex h-6 gap-px" role="img" aria-label="Last 30 days: 30 days operational" data-days="uuuuuuuuuuuuuuuuuuuuuuuuuuuuuu"></div>
      <p class="font-mono text-xs text-slate-300 tabular-nums @2xl:text-right">99.98%</p>
    </li>
    <li class="grid items-center gap-2 px-4 py-3 @2xl:grid-cols-[13rem_1fr_4.5rem] @2xl:gap-4">
      <p class="flex items-center gap-2 text-sm"><span class="size-2 rounded-full bg-emerald-400" aria-hidden="true"></span><span class="font-medium text-white">Auth</span><span class="text-xs text-slate-400">Operational</span></p>
      <div class="tdb-ops-strip flex h-6 gap-px" role="img" aria-label="Last 30 days: 29 days operational, 1 day degraded" data-days="uuuuuuuuuuuuduuuuuuuuuuuuuuuuu"></div>
      <p class="font-mono text-xs text-slate-300 tabular-nums @2xl:text-right">99.95%</p>
    </li>
    <li class="grid items-center gap-2 px-4 py-3 @2xl:grid-cols-[13rem_1fr_4.5rem] @2xl:gap-4">
      <p class="flex items-center gap-2 text-sm"><span class="size-2 rounded-full bg-amber-400" aria-hidden="true"></span><span class="font-medium text-white">Search</span><span class="text-xs text-amber-300">Degraded</span></p>
      <div class="tdb-ops-strip flex h-6 gap-px" role="img" aria-label="Last 30 days: 26 days operational, 3 days degraded, 1 day with an outage" data-days="uuuuuuuuuuuuuuuuuuuuduuuxuuudd"></div>
      <p class="font-mono text-xs text-slate-300 tabular-nums @2xl:text-right">99.41%</p>
    </li>
    <li class="grid items-center gap-2 px-4 py-3 @2xl:grid-cols-[13rem_1fr_4.5rem] @2xl:gap-4">
      <p class="flex items-center gap-2 text-sm"><span class="size-2 rounded-full bg-emerald-400" aria-hidden="true"></span><span class="font-medium text-white">Payment webhooks</span><span class="text-xs text-slate-400">Operational</span></p>
      <div class="tdb-ops-strip flex h-6 gap-px" role="img" aria-label="Last 30 days: 29 days operational, 1 day with an outage" data-days="uuuuuuuxuuuuuuuuuuuuuuuuuuuuuu"></div>
      <p class="font-mono text-xs text-slate-300 tabular-nums @2xl:text-right">99.87%</p>
    </li>
  </ul>
  <div class="mt-5 grid gap-4 @2xl:grid-cols-2">
    <section aria-labelledby="tdb-ops-nodes" class="rounded-xl bg-white/[0.03] p-4 ring-1 ring-white/10">
      <h3 id="tdb-ops-nodes" class="text-sm font-semibold text-white">Nodes</h3>
      <dl class="mt-3 space-y-3 font-mono text-xs">
        <div><div class="flex justify-between"><dt class="text-slate-300">web-01 cpu</dt><dd class="text-slate-200 tabular-nums">38%</dd></div><div class="mt-1 h-1.5 rounded-full bg-white/10"><div class="h-full w-[38%] rounded-full bg-sky-400"></div></div></div>
        <div><div class="flex justify-between"><dt class="text-slate-300">web-02 cpu</dt><dd class="text-slate-200 tabular-nums">44%</dd></div><div class="mt-1 h-1.5 rounded-full bg-white/10"><div class="h-full w-[44%] rounded-full bg-sky-400"></div></div></div>
        <div><div class="flex justify-between"><dt class="text-slate-300">search-01 memory</dt><dd class="text-amber-300 tabular-nums">86%</dd></div><div class="mt-1 h-1.5 rounded-full bg-white/10"><div class="h-full w-[86%] rounded-full bg-amber-400"></div></div></div>
        <div><div class="flex justify-between"><dt class="text-slate-300">db-primary disk</dt><dd class="text-slate-200 tabular-nums">61%</dd></div><div class="mt-1 h-1.5 rounded-full bg-white/10"><div class="h-full w-[61%] rounded-full bg-sky-400"></div></div></div>
      </dl>
    </section>
    <section aria-labelledby="tdb-ops-deploys" class="rounded-xl bg-white/[0.03] p-4 ring-1 ring-white/10">
      <h3 id="tdb-ops-deploys" class="text-sm font-semibold text-white">Recent deploys</h3>
      <ol class="mt-3 space-y-2.5 font-mono text-xs">
        <li class="flex gap-3"><span class="text-slate-400">13:48</span><span class="min-w-0 flex-1 text-slate-200"><span class="text-sky-300">a3f9c21</span> checkout: retry idempotent POSTs</span><span class="text-emerald-400">ok</span></li>
        <li class="flex gap-3"><span class="text-slate-400">12:10</span><span class="min-w-0 flex-1 text-slate-200"><span class="text-sky-300">7be0d54</span> search: raise shard heap to 6 GB</span><span class="text-amber-300">watch</span></li>
        <li class="flex gap-3"><span class="text-slate-400">09:31</span><span class="min-w-0 flex-1 text-slate-200"><span class="text-sky-300">19c4e8a</span> auth: rotate session signing key</span><span class="text-emerald-400">ok</span></li>
      </ol>
    </section>
  </div>
</div>

Widgets that react

Pieces that respond to the reader or to their own width: a date-range segmented control that swaps the numbers, a notifications popover, a settings overview with switches and a widget driven by container queries.

Date-range segmented control that updates the numbers

  • has-checked:
  • has-focus-visible:
  • sr-only radio
  • aria-live
  • classList

The segmented control is four real radio buttons hidden with sr-only inside their labels, so arrow keys move between ranges and has-checked:bg-white with has-checked:shadow-xs draws the selected segment without any JS. A change listener swaps the KPI values, the bar heights and the axis labels from one data object, and replaces delta colors with full class names such as bg-rose-50 and text-rose-800 so Tailwind can find them. A polite live region announces the new range.

View code
<div id="tdb-range" class="@container w-full max-w-3xl rounded-2xl bg-white p-5 text-slate-900 shadow-sm ring-1 ring-slate-200">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <h2 class="text-base font-semibold text-slate-900">Store performance</h2>
    <fieldset>
      <legend class="sr-only">Date range</legend>
      <div class="flex rounded-lg bg-slate-100 p-1 text-sm font-medium">
        <label class="relative cursor-pointer rounded-md px-2.5 py-1.5 text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-cyan-700 sm:px-3"><input type="radio" name="tdb-range" value="7" class="sr-only">7D</label>
        <label class="relative cursor-pointer rounded-md px-2.5 py-1.5 text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-cyan-700 sm:px-3"><input type="radio" name="tdb-range" value="30" class="sr-only" checked>30D</label>
        <label class="relative cursor-pointer rounded-md px-2.5 py-1.5 text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-cyan-700 sm:px-3"><input type="radio" name="tdb-range" value="90" class="sr-only">90D</label>
        <label class="relative cursor-pointer rounded-md px-2.5 py-1.5 text-slate-600 hover:text-slate-900 has-checked:bg-white has-checked:text-slate-900 has-checked:shadow-xs has-focus-visible:outline-2 has-focus-visible:outline-cyan-700 sm:px-3"><input type="radio" name="tdb-range" value="365" class="sr-only">12M</label>
      </div>
    </fieldset>
  </div>
  <dl class="mt-5 grid gap-3 @lg:grid-cols-3">
    <div class="rounded-xl bg-slate-50 p-4"><dt class="text-sm text-slate-600">Revenue</dt><dd class="mt-1 flex items-baseline justify-between gap-2"><span id="tdb-range-rev" class="text-2xl font-semibold tabular-nums">$41,260</span><span id="tdb-range-rev-d" class="rounded-md bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-800 tabular-nums">+11.8%</span></dd></div>
    <div class="rounded-xl bg-slate-50 p-4"><dt class="text-sm text-slate-600">Orders</dt><dd class="mt-1 flex items-baseline justify-between gap-2"><span id="tdb-range-ord" class="text-2xl font-semibold tabular-nums">611</span><span id="tdb-range-ord-d" class="rounded-md bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-800 tabular-nums">+9.4%</span></dd></div>
    <div class="rounded-xl bg-slate-50 p-4"><dt class="text-sm text-slate-600">Conversion</dt><dd class="mt-1 flex items-baseline justify-between gap-2"><span id="tdb-range-cvr" class="text-2xl font-semibold tabular-nums">3.1%</span><span id="tdb-range-cvr-d" class="rounded-md bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-800 tabular-nums">+0.2 pts</span></dd></div>
  </dl>
  <div class="mt-5">
    <div id="tdb-range-chart" class="flex h-36 items-end gap-1 border-b border-slate-200 sm:gap-2" role="img" aria-label="Revenue for the last 30 days in 12 buckets, total $41,260">
      <div class="tdb-range-bar h-[40%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[46%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[42%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[55%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[51%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[60%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[58%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[66%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[71%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[69%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[82%] flex-1 rounded-t bg-cyan-700/25 motion-safe:transition-[height] motion-safe:duration-300"></div>
      <div class="tdb-range-bar h-[90%] flex-1 rounded-t bg-cyan-700 motion-safe:transition-[height] motion-safe:duration-300"></div>
    </div>
    <div class="mt-2 flex justify-between text-xs text-slate-600" aria-hidden="true"><span id="tdb-range-from">30 Sep</span><span id="tdb-range-to">28 Oct</span></div>
  </div>
  <p id="tdb-range-status" class="sr-only" aria-live="polite"></p>
</div>

Notifications panel in a popover

  • popover
  • popovertarget
  • data-unread:
  • group-data-unread:block
  • inset-auto

The bell is a button with popovertarget, so the browser opens the panel in the top layer, closes it on Escape or an outside click and exposes the expanded state, with no JS for any of that. inset-auto m-0 cancels the centered popover defaults and fixed top and right offsets pin it under the header. Unread rows carry a data-unread attribute that data-unread:bg-sky-50 and group-data-unread:block style, so Mark all as read only removes attributes and updates the badge.

View code
<div class="min-h-[520px] bg-slate-100 text-slate-900">
  <header class="flex h-16 items-center justify-between gap-3 bg-slate-900 px-4 text-white sm:px-6">
    <span class="flex items-center gap-2 font-semibold"><svg viewBox="0 0 28 28" class="size-7" aria-hidden="true"><rect width="28" height="28" rx="7" class="fill-sky-400"/><path d="M8 19V9h3v4h6V9h3v10h-3v-3h-6v3z" class="fill-slate-900"/></svg>Harbor HQ</span>
    <button type="button" popovertarget="tdb-nt-panel" class="relative grid size-11 place-items-center rounded-lg text-slate-200 hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">
      <span id="tdb-nt-sr" class="sr-only">Notifications, 3 unread</span>
      <svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9a6 6 0 1 1 12 0c0 6 2.5 8 2.5 8h-17S6 15 6 9M10 20.5h4"/></svg>
      <span id="tdb-nt-badge" class="absolute top-1 right-1 grid h-5 min-w-5 place-items-center rounded-full bg-rose-500 px-1 text-[11px] font-semibold text-white ring-2 ring-slate-900" aria-hidden="true">3</span>
    </button>
  </header>
  <div class="grid gap-4 p-4 sm:grid-cols-2 sm:p-6">
    <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="text-sm text-slate-600">Open requests</p><p class="mt-1 text-2xl font-semibold tabular-nums">27</p></div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="text-sm text-slate-600">Berths free tonight</p><p class="mt-1 text-2xl font-semibold tabular-nums">9 of 40</p></div>
    <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200 sm:col-span-2"><p class="text-sm text-slate-600">Open the bell for updates from the harbour team.</p></div>
  </div>
  <div id="tdb-nt-panel" popover aria-labelledby="tdb-nt-title" class="fixed inset-auto top-[4.5rem] right-3 m-0 w-[min(24rem,calc(100vw-1.5rem))] rounded-xl border-0 bg-white p-0 text-slate-900 shadow-xl ring-1 ring-slate-900/10">
    <div class="flex items-center justify-between gap-3 border-b border-slate-200 px-4 py-3">
      <h2 id="tdb-nt-title" class="text-sm font-semibold text-slate-900">Notifications</h2>
      <button id="tdb-nt-readall" type="button" class="min-h-9 rounded-md px-2 text-sm font-medium text-sky-700 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-600 disabled:text-slate-400 disabled:hover:bg-transparent">Mark all as read</button>
    </div>
    <ul class="max-h-80 divide-y divide-slate-100 overflow-y-auto">
      <li data-unread class="tdb-nt-item group flex gap-3 px-4 py-3 data-unread:bg-sky-50">
        <svg viewBox="0 0 36 36" class="size-9 shrink-0" aria-hidden="true"><circle cx="18" cy="18" r="18" class="fill-violet-200"/><text x="18" y="23" text-anchor="middle" class="fill-violet-900 text-xs font-semibold">AB</text></svg>
        <div class="min-w-0 flex-1 text-sm"><p class="text-slate-700"><span class="font-semibold text-slate-900">Ama Boateng</span> commented on <span class="font-medium text-slate-900">Winter mooring rates</span></p><p class="mt-0.5 text-xs text-slate-600">4 minutes ago</p></div>
        <span class="mt-1.5 hidden size-2 shrink-0 rounded-full bg-sky-600 group-data-unread:block" role="img" aria-label="Unread"></span>
      </li>
      <li data-unread class="tdb-nt-item group flex gap-3 px-4 py-3 data-unread:bg-sky-50">
        <svg viewBox="0 0 36 36" class="size-9 shrink-0" aria-hidden="true"><circle cx="18" cy="18" r="18" class="fill-amber-200"/><path d="M18 10v10M18 24.5v1" class="stroke-amber-900" stroke-width="2.5" stroke-linecap="round"/></svg>
        <div class="min-w-0 flex-1 text-sm"><p class="text-slate-700">Gale warning for Thursday, <span class="font-medium text-slate-900">3 bookings</span> may need moving</p><p class="mt-0.5 text-xs text-slate-600">32 minutes ago</p></div>
        <span class="mt-1.5 hidden size-2 shrink-0 rounded-full bg-sky-600 group-data-unread:block" role="img" aria-label="Unread"></span>
      </li>
      <li data-unread class="tdb-nt-item group flex gap-3 px-4 py-3 data-unread:bg-sky-50">
        <svg viewBox="0 0 36 36" class="size-9 shrink-0" aria-hidden="true"><circle cx="18" cy="18" r="18" class="fill-emerald-200"/><path d="m12 18.5 4 4 8-9" fill="none" class="stroke-emerald-900" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
        <div class="min-w-0 flex-1 text-sm"><p class="text-slate-700">Invoice <span class="font-medium text-slate-900">HB-1180</span> was paid by Sea Otter Charters</p><p class="mt-0.5 text-xs text-slate-600">1 hour ago</p></div>
        <span class="mt-1.5 hidden size-2 shrink-0 rounded-full bg-sky-600 group-data-unread:block" role="img" aria-label="Unread"></span>
      </li>
      <li class="tdb-nt-item group flex gap-3 px-4 py-3 data-unread:bg-sky-50">
        <svg viewBox="0 0 36 36" class="size-9 shrink-0" aria-hidden="true"><circle cx="18" cy="18" r="18" class="fill-sky-200"/><text x="18" y="23" text-anchor="middle" class="fill-sky-900 text-xs font-semibold">JL</text></svg>
        <div class="min-w-0 flex-1 text-sm"><p class="text-slate-700"><span class="font-semibold text-slate-900">Jonah Lee</span> assigned you <span class="font-medium text-slate-900">Pontoon C inspection</span></p><p class="mt-0.5 text-xs text-slate-600">Yesterday</p></div>
        <span class="mt-1.5 hidden size-2 shrink-0 rounded-full bg-sky-600 group-data-unread:block" role="img" aria-label="Unread"></span>
      </li>
    </ul>
    <a href="#" class="block rounded-b-xl border-t border-slate-200 px-4 py-3 text-center text-sm font-medium text-slate-700 hover:bg-slate-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600">View all activity</a>
  </div>
</div>

Settings overview with switches

  • role="switch"
  • peer-checked:translate-x-5.5
  • has-checked:bg-teal-700
  • @xl:grid-cols-2
  • motion-safe:transition

The four cards go two by two once the wrapper reaches @xl:, so the same overview fits a full page or a side panel. Each switch is a real checkbox with role="switch" hidden by sr-only; the track reads its state with has-checked:bg-teal-700 and the thumb with peer-checked:translate-x-5.5, and has-focus-visible: draws the keyboard ring on the track. The slide sits inside motion-safe:, the label wraps the whole row so the touch target is 44px tall, and no JS is needed.

View code
<div class="@container w-full max-w-3xl text-slate-900">
  <h2 class="text-lg font-semibold tracking-tight text-slate-900">Settings</h2>
  <div class="mt-4 grid gap-4 @xl:grid-cols-2">
    <section aria-labelledby="tdb-st-profile" class="rounded-2xl bg-white p-5 ring-1 ring-slate-200">
      <h3 id="tdb-st-profile" class="text-sm font-semibold text-slate-900">Profile</h3>
      <div class="mt-4 flex items-center gap-3">
        <svg viewBox="0 0 48 48" class="size-12 shrink-0" aria-hidden="true"><circle cx="24" cy="24" r="24" class="fill-teal-100"/><text x="24" y="30" text-anchor="middle" class="fill-teal-900 text-base font-semibold">JE</text></svg>
        <div class="min-w-0"><p class="truncate font-medium">Jonas Eberhardt</p><p class="truncate text-sm text-slate-600">jonas@example.com</p></div>
      </div>
      <a href="#" class="mt-4 inline-flex min-h-10 items-center rounded-lg px-3.5 text-sm font-medium text-slate-800 ring-1 ring-slate-300 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Edit profile</a>
    </section>
    <section aria-labelledby="tdb-st-plan" class="rounded-2xl bg-white p-5 ring-1 ring-slate-200">
      <div class="flex items-center justify-between gap-2"><h3 id="tdb-st-plan" class="text-sm font-semibold text-slate-900">Plan and billing</h3><span class="rounded-full bg-teal-50 px-2 py-0.5 text-xs font-medium text-teal-800">Team</span></div>
      <p class="mt-3 text-sm text-slate-700"><span class="font-semibold text-slate-900">$48</span> per month, renews 14 November 2026</p>
      <div class="mt-4 flex justify-between text-xs text-slate-600"><span>Seats</span><span class="tabular-nums">9 of 12</span></div>
      <div class="mt-1 h-2 rounded-full bg-slate-100" role="progressbar" aria-label="Seats used" aria-valuemin="0" aria-valuemax="12" aria-valuenow="9"><div class="h-full w-3/4 rounded-full bg-teal-700"></div></div>
      <a href="#" class="mt-4 inline-block rounded text-sm font-medium text-teal-800 hover:underline focus-visible:outline-2 focus-visible:outline-teal-700">Manage billing</a>
    </section>
    <section aria-labelledby="tdb-st-sec" class="rounded-2xl bg-white p-5 ring-1 ring-slate-200">
      <h3 id="tdb-st-sec" class="text-sm font-semibold text-slate-900">Security</h3>
      <label class="mt-2 flex min-h-11 cursor-pointer items-center justify-between gap-4">
        <span class="text-sm text-slate-800">Two-factor sign-in</span>
        <span class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-slate-500 has-checked:bg-teal-700 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-teal-700 motion-safe:transition-colors">
          <input type="checkbox" role="switch" class="peer sr-only" checked>
          <span class="size-5 translate-x-0.5 rounded-full bg-white shadow-xs peer-checked:translate-x-5.5 motion-safe:transition-transform" aria-hidden="true"></span>
        </span>
      </label>
      <p class="mt-2 text-sm text-slate-600">3 active sessions on 2 devices</p>
      <p class="mt-3 flex items-start gap-2 rounded-lg bg-amber-50 p-3 text-xs text-amber-900"><svg viewBox="0 0 20 20" class="mt-px size-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M10 3 2 17h16zM10 8v4M10 14.5v.5"/></svg>Password last changed 214 days ago.</p>
    </section>
    <section aria-labelledby="tdb-st-notif" class="rounded-2xl bg-white p-5 ring-1 ring-slate-200">
      <h3 id="tdb-st-notif" class="text-sm font-semibold text-slate-900">Email notifications</h3>
      <div class="mt-2 divide-y divide-slate-100">
        <label class="flex min-h-11 cursor-pointer items-center justify-between gap-4">
          <span class="text-sm text-slate-800">Weekly digest</span>
          <span class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-slate-500 has-checked:bg-teal-700 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-teal-700 motion-safe:transition-colors">
            <input type="checkbox" role="switch" class="peer sr-only" checked>
            <span class="size-5 translate-x-0.5 rounded-full bg-white shadow-xs peer-checked:translate-x-5.5 motion-safe:transition-transform" aria-hidden="true"></span>
          </span>
        </label>
        <label class="flex min-h-11 cursor-pointer items-center justify-between gap-4">
          <span class="text-sm text-slate-800">Mentions and replies</span>
          <span class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-slate-500 has-checked:bg-teal-700 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-teal-700 motion-safe:transition-colors">
            <input type="checkbox" role="switch" class="peer sr-only" checked>
            <span class="size-5 translate-x-0.5 rounded-full bg-white shadow-xs peer-checked:translate-x-5.5 motion-safe:transition-transform" aria-hidden="true"></span>
          </span>
        </label>
        <label class="flex min-h-11 cursor-pointer items-center justify-between gap-4">
          <span class="text-sm text-slate-800">Product updates</span>
          <span class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-slate-500 has-checked:bg-teal-700 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-teal-700 motion-safe:transition-colors">
            <input id="tdb-st-updates" type="checkbox" role="switch" class="peer sr-only">
            <span class="size-5 translate-x-0.5 rounded-full bg-white shadow-xs peer-checked:translate-x-5.5 motion-safe:transition-transform" aria-hidden="true"></span>
          </span>
        </label>
      </div>
    </section>
  </div>
</div>

Widget that adapts to its own width

  • @container
  • @xs:block
  • @md:flex-row
  • @xl:grid
  • resize-x

The widget never looks at the viewport: it is an @container, so @xs:block adds the sparkline at 20rem, @md:flex-row puts number and chart side by side at 28rem and @xl:grid reveals the plan breakdown at 36rem. Drag the corner of the resize-x box or use the width buttons, which only set an inline width and aria-pressed; a ResizeObserver prints the current width. Drop the same markup into a narrow sidebar slot and it picks the compact layout by itself.

View code
<div class="w-full max-w-3xl">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <p class="text-sm text-slate-700">Widget width <span id="tdb-rs-px" class="font-mono font-medium text-slate-900 tabular-nums">auto</span></p>
    <div role="group" aria-label="Widget width" class="flex rounded-lg bg-white p-1 text-sm font-medium ring-1 ring-slate-200">
      <button type="button" data-w="260px" aria-pressed="false" class="tdb-rs-btn min-h-9 rounded-md px-3 text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-fuchsia-700 aria-pressed:bg-fuchsia-700 aria-pressed:text-white">Narrow</button>
      <button type="button" data-w="420px" aria-pressed="false" class="tdb-rs-btn min-h-9 rounded-md px-3 text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-fuchsia-700 aria-pressed:bg-fuchsia-700 aria-pressed:text-white">Medium</button>
      <button type="button" data-w="100%" aria-pressed="true" class="tdb-rs-btn min-h-9 rounded-md px-3 text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-fuchsia-700 aria-pressed:bg-fuchsia-700 aria-pressed:text-white">Wide</button>
    </div>
  </div>
  <div id="tdb-rs-box" class="mt-3 w-full max-w-full min-w-[200px] resize-x overflow-hidden rounded-2xl border-2 border-dashed border-slate-300 p-2">
    <article class="@container rounded-xl bg-white p-4 text-slate-900 ring-1 ring-slate-200">
      <div class="flex flex-col gap-4 @md:flex-row @md:items-center">
        <div class="shrink-0 @md:w-44">
          <h2 class="text-sm font-medium text-slate-600">Active subscriptions</h2>
          <p class="mt-1 text-3xl font-semibold tracking-tight tabular-nums">2,318</p>
          <p class="mt-1 text-xs font-medium text-emerald-700">+86 since 1 October</p>
        </div>
        <svg viewBox="0 0 200 60" preserveAspectRatio="none" class="hidden h-14 w-full min-w-0 flex-1 @xs:block" aria-hidden="true"><path d="M0,60 L0,44 18,46 36,40 54,41 72,35 90,37 108,30 126,28 144,24 162,22 180,15 200,12 200,60 Z" class="fill-fuchsia-600/10"/><path d="M0,44 18,46 36,40 54,41 72,35 90,37 108,30 126,28 144,24 162,22 180,15 200,12" class="fill-none stroke-fuchsia-700" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>
        <dl class="hidden shrink-0 gap-2 text-sm @xl:grid @xl:w-36">
          <div class="flex justify-between gap-3 rounded-md bg-slate-50 px-2.5 py-1.5"><dt class="text-slate-600">Monthly</dt><dd class="font-medium tabular-nums">1,640</dd></div>
          <div class="flex justify-between gap-3 rounded-md bg-slate-50 px-2.5 py-1.5"><dt class="text-slate-600">Annual</dt><dd class="font-medium tabular-nums">678</dd></div>
        </dl>
      </div>
    </article>
  </div>
</div>

Empty, loading, dark and branded

The states every dashboard needs sooner or later: a first-run empty state, a loading skeleton driven by aria-busy, a dark mode toggle on its own wrapper and white-label colors from prefixed @theme tokens.

First-run empty state with a setup checklist

  • border-dashed
  • aria-[current=step]:
  • stroke-dasharray
  • min-h-11
  • sm:flex-row

An empty chart should say what is missing and offer the next step, so the dashed box holds a small inline SVG of an empty axis, a plain sentence and two buttons that stack on phones and sit in a row from sm:. The checklist below is an ordered list where aria-current="step" marks the active item and aria-[current=step]: styles it, so the visual state and the screen reader state come from one attribute. Done steps carry a check icon and visible text, not color alone.

View code
<div class="w-full max-w-2xl rounded-2xl bg-white p-5 text-slate-900 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <div class="flex items-center justify-between gap-3">
    <h2 class="text-base font-semibold text-slate-900">Revenue</h2>
    <span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-700">No source connected</span>
  </div>
  <div class="mt-4 flex flex-col items-center rounded-xl border-2 border-dashed border-slate-300 px-5 py-8 text-center">
    <svg viewBox="0 0 120 64" class="h-16 w-28 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M8 4v52h108"/><path d="M8 44c18 0 22-14 40-14s22 10 40-6 20-12 28-12" stroke-dasharray="4 6"/></svg>
    <h3 class="mt-4 text-base font-semibold text-slate-900">No revenue data yet</h3>
    <p class="mt-1 max-w-sm text-sm text-slate-600">Connect a payment provider or import a CSV file and this chart fills in within a few minutes.</p>
    <div class="mt-5 flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
      <a href="#" class="inline-flex min-h-11 items-center justify-center rounded-lg bg-emerald-800 px-5 text-sm font-semibold text-white hover:bg-emerald-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-800">Connect a source</a>
      <a href="#" class="inline-flex min-h-11 items-center justify-center rounded-lg px-5 text-sm font-semibold text-slate-800 ring-1 ring-slate-300 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-800">Import CSV</a>
    </div>
  </div>
  <h3 class="mt-6 text-sm font-semibold text-slate-900">Get your first report</h3>
  <ol class="mt-3 space-y-2 text-sm">
    <li class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-600 aria-[current=step]:bg-emerald-50 aria-[current=step]:text-emerald-950 aria-[current=step]:ring-1 aria-[current=step]:ring-emerald-700/20">
      <span class="grid size-6 shrink-0 place-items-center rounded-full bg-emerald-800 text-white"><svg viewBox="0 0 16 16" class="size-3.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3.5 8.5 3 3 6-7"/></svg></span>
      <span class="line-through">Create the workspace</span><span class="sr-only">, done</span>
    </li>
    <li aria-current="step" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-600 aria-[current=step]:bg-emerald-50 aria-[current=step]:text-emerald-950 aria-[current=step]:ring-1 aria-[current=step]:ring-emerald-700/20">
      <span class="grid size-6 shrink-0 place-items-center rounded-full border-2 border-emerald-800 text-xs font-semibold text-emerald-900" aria-hidden="true">2</span>
      <span class="font-medium">Connect a data source</span>
    </li>
    <li class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-600 aria-[current=step]:bg-emerald-50 aria-[current=step]:text-emerald-950 aria-[current=step]:ring-1 aria-[current=step]:ring-emerald-700/20">
      <span class="grid size-6 shrink-0 place-items-center rounded-full border-2 border-slate-300 text-xs font-semibold text-slate-600" aria-hidden="true">3</span>
      <span>Invite your team</span>
    </li>
  </ol>
</div>

Loading skeleton driven by aria-busy

  • aria-busy
  • group-aria-busy:hidden
  • motion-safe:animate-pulse
  • aria-live
  • setTimeout

The region carries aria-busy, and every card holds its real content next to a skeleton: group-aria-busy:hidden hides the data and hidden group-aria-busy:block shows the gray blocks, so the script only flips one attribute. The pulse runs inside motion-safe:, and a polite live region says when loading starts and when the numbers are back. Refresh stays disabled while the fake request runs.

View code
<div class="w-full max-w-3xl rounded-2xl bg-white p-5 text-slate-900 shadow-sm ring-1 ring-slate-200">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <div>
      <h2 class="text-base font-semibold text-slate-900">Support desk</h2>
      <p id="tdb-sk-time" class="text-xs text-slate-600">Updated 14:05</p>
    </div>
    <button id="tdb-sk-refresh" type="button" class="inline-flex min-h-10 items-center gap-2 rounded-lg px-3.5 text-sm font-medium text-slate-800 ring-1 ring-slate-300 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-wait disabled:text-slate-500"><svg viewBox="0 0 20 20" class="size-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 10a6 6 0 1 1-1.8-4.3M16 3.5V7h-3.5"/></svg>Refresh</button>
  </div>
  <div id="tdb-sk" class="group mt-4" aria-busy="false">
    <div class="grid gap-3 sm:grid-cols-3">
      <div class="rounded-xl bg-slate-50 p-4">
        <div class="group-aria-busy:hidden"><p class="text-sm text-slate-600">Open tickets</p><p id="tdb-sk-open" class="mt-1 text-2xl font-semibold tabular-nums">128</p></div>
        <div class="hidden space-y-2.5 py-1 group-aria-busy:block motion-safe:animate-pulse" aria-hidden="true"><div class="h-3 w-24 rounded bg-slate-200"></div><div class="h-6 w-16 rounded bg-slate-200"></div></div>
      </div>
      <div class="rounded-xl bg-slate-50 p-4">
        <div class="group-aria-busy:hidden"><p class="text-sm text-slate-600">First reply, median</p><p id="tdb-sk-reply" class="mt-1 text-2xl font-semibold tabular-nums">1h 12m</p></div>
        <div class="hidden space-y-2.5 py-1 group-aria-busy:block motion-safe:animate-pulse" aria-hidden="true"><div class="h-3 w-28 rounded bg-slate-200"></div><div class="h-6 w-20 rounded bg-slate-200"></div></div>
      </div>
      <div class="rounded-xl bg-slate-50 p-4">
        <div class="group-aria-busy:hidden"><p class="text-sm text-slate-600">Satisfaction</p><p id="tdb-sk-csat" class="mt-1 text-2xl font-semibold tabular-nums">94%</p></div>
        <div class="hidden space-y-2.5 py-1 group-aria-busy:block motion-safe:animate-pulse" aria-hidden="true"><div class="h-3 w-20 rounded bg-slate-200"></div><div class="h-6 w-14 rounded bg-slate-200"></div></div>
      </div>
    </div>
    <ul class="mt-4 divide-y divide-slate-100 text-sm group-aria-busy:hidden">
      <li class="flex items-center gap-3 py-2.5"><span class="rounded bg-rose-50 px-1.5 py-0.5 text-xs font-medium text-rose-800">Urgent</span><span class="min-w-0 flex-1 truncate">Cannot export invoices to PDF</span><span class="text-xs text-slate-600">6m</span></li>
      <li class="flex items-center gap-3 py-2.5"><span class="rounded bg-amber-50 px-1.5 py-0.5 text-xs font-medium text-amber-800">High</span><span class="min-w-0 flex-1 truncate">Team invite email never arrived</span><span class="text-xs text-slate-600">21m</span></li>
      <li class="flex items-center gap-3 py-2.5"><span class="rounded bg-slate-100 px-1.5 py-0.5 text-xs font-medium text-slate-700">Normal</span><span class="min-w-0 flex-1 truncate">How do I change the billing currency?</span><span class="text-xs text-slate-600">48m</span></li>
    </ul>
    <ul class="mt-4 hidden space-y-4 py-1 group-aria-busy:block motion-safe:animate-pulse" aria-hidden="true">
      <li class="flex items-center gap-3"><div class="h-5 w-14 rounded bg-slate-200"></div><div class="h-3 flex-1 rounded bg-slate-200"></div><div class="h-3 w-6 rounded bg-slate-200"></div></li>
      <li class="flex items-center gap-3"><div class="h-5 w-14 rounded bg-slate-200"></div><div class="h-3 flex-1 rounded bg-slate-200"></div><div class="h-3 w-6 rounded bg-slate-200"></div></li>
      <li class="flex items-center gap-3"><div class="h-5 w-14 rounded bg-slate-200"></div><div class="h-3 flex-1 rounded bg-slate-200"></div><div class="h-3 w-6 rounded bg-slate-200"></div></li>
    </ul>
  </div>
  <p id="tdb-sk-status" class="sr-only" aria-live="polite"></p>
</div>

Dark mode toggle on its own wrapper

  • dark:
  • @custom-variant dark
  • scheme-light dark:scheme-dark
  • aria-pressed
  • dark:hidden

With @custom-variant dark (&:where(.dark, .dark *)) in the stylesheet, dark: matches the element that carries the dark class and everything inside it, so the toggle adds dark to this wrapper only and the rest of the page is untouched. Every color comes as a pair such as bg-white dark:bg-slate-950, and scheme-light dark:scheme-dark switches native parts like scrollbars and form controls too. The button sets aria-pressed, and the sun and moon icons swap with dark:hidden and dark:block.

View code
<div id="tdb-dm" class="scheme-light dark:scheme-dark">
  <div class="min-h-[480px] bg-white p-4 text-slate-900 sm:p-6 dark:bg-slate-950 dark:text-slate-100">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <div>
        <h2 class="text-lg font-semibold tracking-tight">Fleet overview</h2>
        <p class="text-sm text-slate-600 dark:text-slate-400">Northline Couriers, Tuesday</p>
      </div>
      <button id="tdb-dm-toggle" type="button" aria-pressed="false" class="inline-flex min-h-10 items-center gap-2 rounded-lg px-3 text-sm font-medium text-slate-700 ring-1 ring-slate-300 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600 dark:text-slate-200 dark:ring-white/15 dark:hover:bg-white/10 dark:focus-visible:outline-orange-400">
        <svg viewBox="0 0 24 24" class="size-5 dark:hidden" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg>
        <svg viewBox="0 0 24 24" class="hidden size-5 dark:block" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.3 5.3l1.4 1.4M17.3 17.3l1.4 1.4M5.3 18.7l1.4-1.4M17.3 6.7l1.4-1.4"/></svg>
        Dark mode
      </button>
    </div>
    <dl class="mt-5 grid gap-3 sm:grid-cols-3">
      <div class="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200 dark:bg-white/5 dark:ring-white/10"><dt class="text-sm text-slate-600 dark:text-slate-400">Vans on route</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">64 <span class="text-base font-normal text-slate-600 dark:text-slate-400">of 72</span></dd></div>
      <div class="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200 dark:bg-white/5 dark:ring-white/10"><dt class="text-sm text-slate-600 dark:text-slate-400">On-time rate</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">93.4%</dd></div>
      <div class="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200 dark:bg-white/5 dark:ring-white/10"><dt class="text-sm text-slate-600 dark:text-slate-400">Parcels left</dt><dd class="mt-1 text-2xl font-semibold tabular-nums">1,206</dd></div>
    </dl>
    <div class="mt-4 grid gap-4 md:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
      <figure class="rounded-xl p-4 ring-1 ring-slate-200 dark:ring-white/10">
        <figcaption class="text-sm font-semibold">Deliveries per hour</figcaption>
        <div class="mt-4 flex h-32 items-end gap-2" role="img" aria-label="Deliveries per hour from 8:00 to 15:00: 120, 210, 260, 240, 180, 230, 270, 150 so far">
          <div class="h-[44%] flex-1 rounded-t bg-orange-200 dark:bg-orange-400/30"></div>
          <div class="h-[78%] flex-1 rounded-t bg-orange-200 dark:bg-orange-400/30"></div>
          <div class="h-[96%] flex-1 rounded-t bg-orange-200 dark:bg-orange-400/30"></div>
          <div class="h-[89%] flex-1 rounded-t bg-orange-200 dark:bg-orange-400/30"></div>
          <div class="h-[67%] flex-1 rounded-t bg-orange-200 dark:bg-orange-400/30"></div>
          <div class="h-[85%] flex-1 rounded-t bg-orange-200 dark:bg-orange-400/30"></div>
          <div class="h-full flex-1 rounded-t bg-orange-600 dark:bg-orange-400"></div>
          <div class="h-[56%] flex-1 rounded-t border-2 border-dashed border-orange-600 dark:border-orange-400"></div>
        </div>
        <div class="mt-2 flex justify-between text-xs text-slate-600 dark:text-slate-400" aria-hidden="true"><span>08:00</span><span>15:00</span></div>
      </figure>
      <section aria-labelledby="tdb-dm-alerts" class="rounded-xl p-4 ring-1 ring-slate-200 dark:ring-white/10">
        <h3 id="tdb-dm-alerts" class="text-sm font-semibold">Alerts</h3>
        <ul class="mt-3 space-y-3 text-sm">
          <li class="flex gap-2"><span class="mt-1.5 size-2 shrink-0 rounded-full bg-rose-500" aria-hidden="true"></span><span>Van 31 stopped for 22 minutes on Ring Road</span></li>
          <li class="flex gap-2"><span class="mt-1.5 size-2 shrink-0 rounded-full bg-amber-500" aria-hidden="true"></span><span>Depot B loading bay 4 closed until 16:00</span></li>
          <li class="flex gap-2"><span class="mt-1.5 size-2 shrink-0 rounded-full bg-emerald-500" aria-hidden="true"></span><span>Afternoon wave dispatched on time</span></li>
        </ul>
      </section>
    </div>
  </div>
</div>

White-label dashboard from @theme brand tokens

  • @theme
  • bg-tdb-brand
  • rounded-tdb-card
  • has-checked:
  • CSS variables

Four prefixed tokens in @theme, such as --color-tdb-brand and --radius-tdb-card, become real utilities (bg-tdb-brand, text-tdb-brand-ink, rounded-tdb-card) that read the values through CSS variables. Because the utilities point at variables, a class like tdb-brand-clay on the wrapper can redefine the tokens and the whole preview rebrands, corner radius included, without touching a single utility in the markup. The client picker is a radio group styled with has-checked:, and the JS only swaps the wrapper class and the client name.

View code
<div class="w-full max-w-3xl">
  <fieldset class="flex flex-wrap items-center gap-2">
    <legend class="mb-2 w-full text-sm font-medium text-slate-700">Preview the portal as</legend>
    <label class="inline-flex min-h-10 cursor-pointer items-center gap-2 rounded-full bg-white px-3.5 text-sm font-medium text-slate-700 ring-1 ring-slate-300 has-checked:bg-slate-900 has-checked:text-white has-checked:ring-slate-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-slate-900"><input type="radio" name="tdb-brand" value="harbor" data-name="Harbor Mutual" class="tdb-brand-pick sr-only" checked><span class="size-3 rounded-full bg-[#1d4ed8]" aria-hidden="true"></span>Harbor</label>
    <label class="inline-flex min-h-10 cursor-pointer items-center gap-2 rounded-full bg-white px-3.5 text-sm font-medium text-slate-700 ring-1 ring-slate-300 has-checked:bg-slate-900 has-checked:text-white has-checked:ring-slate-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-slate-900"><input type="radio" name="tdb-brand" value="clay" data-name="Claybrook Homes" class="tdb-brand-pick sr-only"><span class="size-3 rounded-full bg-[#9a3412]" aria-hidden="true"></span>Claybrook</label>
    <label class="inline-flex min-h-10 cursor-pointer items-center gap-2 rounded-full bg-white px-3.5 text-sm font-medium text-slate-700 ring-1 ring-slate-300 has-checked:bg-slate-900 has-checked:text-white has-checked:ring-slate-900 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-slate-900"><input type="radio" name="tdb-brand" value="moss" data-name="Mossgate Farms" class="tdb-brand-pick sr-only"><span class="size-3 rounded-full bg-[#3f6212]" aria-hidden="true"></span>Mossgate</label>
  </fieldset>
  <div id="tdb-brand-root" class="mt-4 overflow-hidden rounded-tdb-card bg-white shadow-sm ring-1 ring-slate-200">
    <div class="flex items-center justify-between gap-3 bg-tdb-brand px-5 py-4 text-white">
      <p class="flex items-center gap-2.5 font-semibold"><svg viewBox="0 0 28 28" class="size-7" aria-hidden="true"><rect width="28" height="28" rx="8" class="fill-white/20"/><circle cx="14" cy="14" r="6" class="fill-white"/></svg><span id="tdb-brand-name">Harbor Mutual</span></p>
      <span class="text-sm text-white/85">Client portal</span>
    </div>
    <div class="p-5">
      <div class="grid gap-3 sm:grid-cols-3">
        <div class="rounded-tdb-card bg-tdb-brand-soft p-4"><p class="text-sm text-tdb-brand-ink">Open claims</p><p class="mt-1 text-2xl font-semibold text-tdb-brand-ink tabular-nums">7</p></div>
        <div class="rounded-tdb-card bg-tdb-brand-soft p-4"><p class="text-sm text-tdb-brand-ink">Documents</p><p class="mt-1 text-2xl font-semibold text-tdb-brand-ink tabular-nums">42</p></div>
        <div class="rounded-tdb-card bg-tdb-brand-soft p-4"><p class="text-sm text-tdb-brand-ink">Messages</p><p class="mt-1 text-2xl font-semibold text-tdb-brand-ink tabular-nums">3</p></div>
      </div>
      <div class="mt-5">
        <div class="flex justify-between text-sm"><span class="font-medium text-slate-800">Onboarding</span><span class="text-slate-600 tabular-nums">4 of 6 steps</span></div>
        <div class="mt-1.5 h-2 rounded-full bg-slate-100" role="progressbar" aria-label="Onboarding progress" aria-valuemin="0" aria-valuemax="6" aria-valuenow="4"><div class="h-full w-2/3 rounded-full bg-tdb-brand"></div></div>
      </div>
      <div class="mt-5 flex flex-wrap items-center gap-3">
        <a href="#" class="inline-flex min-h-11 items-center rounded-tdb-card bg-tdb-brand px-5 text-sm font-semibold text-white hover:bg-tdb-brand-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tdb-brand">Upload a document</a>
        <a href="#" class="rounded text-sm font-medium text-tdb-brand-ink underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-tdb-brand">Contact your adviser</a>
      </div>
    </div>
  </div>
</div>

What is a Tailwind dashboard?

Everything on the screen, from the sidebar down to the charts, gets styled by Tailwind CSS utility classes written straight into the markup. No component stylesheet backs any of it up.

Tailwind CSS ships utility classes, not prebuilt components, so a dashboard has to be assembled, copied from a kit, or adopted as a full template.

You can write the whole thing by hand with utilities. Or you can take a UI kit and drop its ready-made tables, modals and cards into a layout you wrote yourself. Templates go furthest, with complete pages for analytics, ecommerce or CRM and the layout already wired. The more you hand off, the more finished work you inherit.

Anyone new to the framework should skim the short explainer on what Tailwind is first, since the utility-first model explains why the markup looks the way it does.

Tailwind does nothing for data. Stat cards, tables and charts read from an API, and the dashboard only styles and arranges whatever comes back.

Tailwind CSS v3 or v4 for a dashboard

Use v4 for any new dashboard. The one exception is a browser list that reaches below Safari 16.4, Chrome 111 or Firefox 128.

Tailwind Labs released v4.0 on January 22, 2025, and the Tailwind CSS docs list v4.3 as the current release as of October 2026.

The compatibility docs set the browser floor at Safari 16.4, Chrome 111 and Firefox 128, and the upgrade guide says v4 will not work in older browsers.

If your cross-browser compatibility requirements are strict, stay on v3.4 until the browser list changes. The upgrade guide gives the same advice.

On v3, configuration sits in tailwind.config.js. Three @tailwind directives load the framework, and a content array tells it which template paths to scan.

v4 moves most of that into the CSS file. Design tokens go in an @theme block, and one line, @import “tailwindcss”, loads everything. Content detection is automatic and skips .gitignore entries and binary files. On Vite projects, the first-party @tailwindcss/vite plugin replaces the PostCSS step.

A v3 config file still loads through the @config directive, but v4 won’t detect it on its own.

Which version a template uses matters more than you’d think. TailAdmin’s free template moved to v4 in release 2.0.1 on February 27, 2025, and Filament 4 shipped on Tailwind CSS v4 on August 12, 2025.

Flowbite’s open-source admin dashboard moved to v4 in its 2.0.0 release in March 2025, but its README still says v3.x, so check a template’s package.json rather than its description.

Mixing generations changes how existing markup renders. v4 renames shadow-sm to shadow-xs, ring to ring-3 and outline-none to outline-hidden, and border utilities now default to currentColor instead of gray-200.

The upgrade tool (npx @tailwindcss/upgrade) handles most of the renames and needs Node.js 20 or higher. Run it on a new branch and read through the diff.

Inherited templates run into a couple of other v4 changes. Hover styles now apply only where the primary input supports hover, and Sass, Less and Stylus are no longer supported, so a template with Sass sources needs rework.

Build a Tailwind dashboard or start from a template?

A custom data model or an existing design system points toward building. A tight deadline for the first screen points toward a template.

A team that already maintains a design system gains little from a template, because its tokens and components fight the template’s own.

Building from scratch gives you full control of markup, naming and data flow, and nothing unused to strip out later. The cost is that tables, modals and dropdowns all have to be sourced or written, and the first usable screen takes the longest to reach.

A template gives you complete pages and wired navigation on day one. In return you take on its Tailwind version and framework, plus example pages and spare dashboards that need removing.

A Tailwind CSS UI kit sits in the middle, with prebuilt components inside your own layout. The shell, routing and data wiring stay your job, which suits teams that want finished components without adopting someone else’s page structure.

Templates hide their cost in the Tailwind generation they were built on. Hand-built dashboards hide theirs in the long tail of components, date pickers and empty states included.

Flowbite’s open-source admin dashboard is a good example of the template side, bundling login, registration, 404 and 500 pages next to its dashboard layouts.

Tailwind dashboard templates and kits worth using

TailAdmin is the strongest free start, and shadcn/ui’s dashboard-01 block suits React teams. Tailwind Plus with Catalyst was the paid option, but it closed to new customers on September 9, 2026, so it now applies only to teams that already hold a license.

Ranking here weighs how current the Tailwind version is against how deep the component set goes, and the license has to be clear on top of that.

OptionStackLicenseVersion note
TailAdmin freeHTML and Alpine.js; React, Next.js, Vue, Angular and Laravel editionsMIT (community edition)v4 since release 2.0.1 (Feb 2025)
shadcn/ui dashboard-01React, installed with the shadcn CLIOpen sourcev4 and React 19 for new projects
Tailwind Plus with CatalystReact and Headless UI; templates on Next.jsProprietary; closed to new customers since Sept 2026Existing license holders keep access; no new sales
Filament 4 and 5Laravel and Livewire admin panelOpen sourcev4 since Filament 4 (Aug 2025); Filament 5 (Jan 2026) keeps it
Flowbite Admin DashboardHTML, Hugo and WebpackMITv4 since release 2.0.0 (Mar 2025); README still says v3.x
  1. TailAdmin. The free tier covers a dashboard, charts, tables, authentication forms and dark mode, though the free edition feels thin next to Pro.
  2. shadcn/ui dashboard-01. A sidebar, chart and data table block that lands in your repo as source code you own. It’s React only, and existing v3 projects keep receiving v3 components until they upgrade.
  3. Tailwind Plus with Catalyst. The Tailwind team’s own React kit and templates, built around React and Headless UI. Closed to new customers since September 9, 2026.
  4. Filament. A full Laravel admin panel built on Livewire, now on version 5 (January 2026). Since v4 it extracts repeated Tailwind class groups into dedicated classes, so the markup isn’t hand-written utility styling.
  5. Tremor. React components for charts and dashboards on Tailwind CSS and Radix UI. Components only, so you get no page shell or routing.
  6. Flowbite Admin Dashboard. MIT-licensed dashboard pages, CRUD layouts and charts. Its package moved to Tailwind v4 in version 2.0.0, though the README still mentions v3.x.

Most of TailAdmin’s depth is behind the Pro paywall. The repository lists 1 unique dashboard in the free edition against 7 in Pro (TailAdmin GitHub, 2026).

Vercel acquired Tremor on January 22, 2025, and Tremor Blocks, formerly a paid product, became free under an MIT license.

React teams new to the pairing can read a walkthrough on using Tailwind CSS in React before picking between shadcn/ui, Tremor and, for existing license holders, Catalyst.

What dashboard template licenses allow

MIT-licensed templates allow commercial use. Tailwind Plus, for existing license holders, allows client work but not resale.

TailAdmin’s community edition and Flowbite’s admin dashboard both ship under the MIT license, with the license file in the repository. Keep that notice with the code and commercial use is covered.

Tailwind Plus is proprietary, and it closed to new customers on September 9, 2026, when Tailwind Labs joined Shopify (Tailwind Labs blog, September 2026). Existing license holders keep their access. A custom site built for a single client is fine, but the license forbids using it to build a site or template that is resold to multiple clients (Tailwind Plus FAQ).

Before sales closed, the Tailwind Plus pricing and checkout pages listed a personal license at $299 one-time, covering 500+ components, every template and Catalyst. Catalyst alone cost $149 one-time for unlimited personal and commercial projects, and the team license covered 25 seats.

None of those prices recurred. Tailwind Labs sold Tailwind Plus as a one-time purchase with no subscription.

Watch for aggregator sites that repost free kits without the license file. Download from the author’s repository instead, where TailAdmin and Flowbite both keep the LICENSE next to the code.

How to build a Tailwind dashboard layout

A fixed sidebar sits beside a column that holds the top bar and the content grid, and the whole thing lives inside one full-height flex shell.

Build the shell first, fill it second, and leave breakpoints for last.

  1. Create the Vite project with Tailwind installed, and add the viewport meta tag to the head, because breakpoint prefixes depend on it
  2. Wrap the page in a flex container at full screen height (flex h-screen)
  3. Add the sidebar as the first child, with a width that never shrinks (w-64 shrink-0)
  4. Add a second column that holds the top bar and a scrolling main area (flex-1 overflow-y-auto)
  5. Fill the main area with a grid of stat cards, then the chart and table region
  6. Add breakpoint prefixes last, starting from the smallest screen

Sidebar and top bar

The sidebar is a fixed-width column with its links in a vertical flex list, and hover and focus variants handle the link states. The top bar is a header (flex items-center justify-between) above the scrolling area. Use sticky top-0 only when the whole column scrolls.

Skip the fixed viewport height on the shell and the whole page scrolls, taking the sidebar along with it.

For the sidebar on its own, a separate walkthrough of the Tailwind sidebar goes into the markup in more depth.

Content grid for stat cards and tables

Stat cards sit in a responsive grid, and the chart and table share the space below.

A typical card grid reads grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4. That gives one column by default, two from sm and four from xl.

Under it, the chart region takes col-span-2 beside a narrower panel, and the table runs full width at the bottom.

shadcn/ui’s dashboard-01 block follows this order, with a row of section cards, an interactive area chart and then a data table.

Responsive breakpoints and a collapsible sidebar

Tailwind’s breakpoints are mobile-first, so an unprefixed class applies at every width and a prefixed one applies from its breakpoint upward.

PrefixMinimum width
sm40rem (640px)
md48rem (768px)
lg64rem (1024px)
xl80rem (1280px)
2xl96rem (1536px)

Reading sm: as “on phones” is the usual mistake. Phone styles are the unprefixed ones, and sm: starts at 640px.

Because Tailwind follows a mobile-first design order, the sidebar is written as hidden lg:flex, absent on small screens and visible from 1024px. Below that width a toggle reveals it as an overlay, and md:max-xl:flex confines a rule to a single range.

Container queries, built into v4, size a card by its parent instead of the viewport (@container, then @md:flex-row). That helps when the content width changes as the sidebar opens, and dashboard-01 marks its main area as @container/main.

TailAdmin’s free template handles this kind of interaction with Alpine.js and ships a collapsible sidebar.

One gotcha with custom breakpoints set in @theme: they need to share one unit. Tailwind’s defaults use rem, and mixing units can reorder the generated utilities so breakpoint classes override each other unexpectedly.

Components a Tailwind dashboard needs

Only the stat cards come from utilities alone. Data tables, overlays such as modals, dropdowns and menus, form controls and icons all need something else.

For tables, TanStack Table supplies logic and state for React, Preact, Vue, Solid, Svelte, Angular, Ember, Lit and Alpine projects, plus a framework-agnostic core package. It brings no markup, styles or themes.

Overlays come from Headless UI (Tailwind Labs, React and Vue) or Radix UI Primitives (React), which provide unstyled dialogs, menus and popovers with keyboard handling and focus management built in. Headless UI also covers the form controls, including Combobox, Listbox, Select, Switch, Checkbox and Radio Group.

Icons are the easy part. Heroicons, from the Tailwind team, ships SVG icons under an MIT license in outline, solid, mini and micro styles.

Going headless has a price. TanStack’s docs describe the library as the opposite of a pre-built table component, so the table HTML and its classes are yours to write, and a guide to the Tailwind table covers that part.

Full component sets differ mostly in who owns the code. shadcn/ui copies component source into your repo, with no hidden abstractions. daisyUI is a pure CSS plugin that adds class names such as btn, card and toggle to Tailwind, with no JavaScript bundle. Preline UI and Flowbite go the other way and ship prebuilt components, with JavaScript plugins for dropdowns, modals, tabs and drawers.

The Node.js website uses Radix Primitives as a significant part of its web design system, according to Radix’s case study.

Overlays carry the accessibility load, because focus order and keyboard behavior have to work before any styling matters.

Adding charts to a Tailwind dashboard

Pick the chart library by how it draws. Chart.js paints a canvas, which Tailwind classes can’t style. SVG libraries output elements that fill-* and stroke-* utilities can reach.

The Chart.js docs say canvas rendering disallows CSS styling, so colors and fonts go through built-in options or a custom plugin.

Recharts is built with React and D3, uses declarative components, and draws native SVG. It is MIT licensed and suits React dashboards.

Tremor offers copy-paste React components for charts and dashboards on top of Tailwind CSS and Radix UI.

On React, Recharts, shadcn/ui’s chart components or Tremor all work. Vue, Svelte and Angular projects can use Chart.js, which has a wrapper for each, or ApexCharts, which adds official Vue 3 and Angular wrappers.

Colors passed to a chart as JavaScript values don’t follow the dark variant. shadcn/ui’s chart config avoids this by referencing CSS variables such as var(–chart-1), so the theme drives the colors.

ApexCharts attaches a size condition to its free license. The Community License covers organizations under $2 million in annual revenue, budget or funding, and a paid Commercial License applies at or above that figure (ApexCharts licensing page, 2026).

Flowbite’s MIT-licensed admin dashboard lists ApexCharts among its third-party components. The template’s MIT license therefore covers the template’s own code and not the chart library, which qualifies the commercial-use point made for MIT templates.

A dedicated guide to Tailwind charts goes further on the styling side.

How to add dark mode to a Tailwind dashboard

Add dark: variants to every color utility, then decide how the theme switches. The operating system setting is the default, and a class on the html element gives you a manual toggle.

Following the system preference costs nothing. The dark variant tracks the prefers-color-scheme media feature out of the box, with no JavaScript and no configuration. The catch is that users can’t override their system setting from inside the dashboard.

A manual toggle takes a bit more work. Override the variant in CSS with @custom-variant dark (&:where(.dark, .dark *)); and set the dark class on the html element with a few lines of JavaScript. A data-theme attribute works the same way with an attribute selector, and a choice between light, dark and system combines localStorage with window.matchMedia.

Each element carries paired classes, such as bg-white dark:bg-gray-800, because a single utility never holds both the light and the dark value (Tailwind CSS docs).

Alpine.js’s Persist plugin saves a value to localStorage, and its docs show a darkMode store built with Alpine.$persist(true).as(‘darkMode_on’).

The classic bug here is a flash of the wrong theme on load. Tailwind’s docs place the theme script inline in the head, and Alpine’s plugin script loads with defer, so a head script that sets the class first is the safer order.

v4 added color-scheme utilities that remove light scrollbars from dark mode.

Contrast needs its own check per theme. shadcn/ui revised its dark mode colors on March 12, 2025 to make them more accessible, so verify color contrast in both themes before shipping.

When a Tailwind dashboard is the wrong fit

Tailwind stops paying off when the work is mostly data. It also struggles for teams with no component framework to host copy-paste kits, and on pages where legacy CSS already owns the styling.

Chart.js’s docs recommend canvas for large datasets because SVG would need thousands of DOM nodes, and canvas gives up utility styling. TanStack’s docs set their headless approach against pre-built grids such as AG Grid, which ship ready-made markup and styles but limit customization to the component’s props and theme APIs.

Without a component framework, the kits get harder to use. Radix UI is React-only, and shadcn/ui and Catalyst are React kits. Vue, Laravel and plain HTML teams have Headless UI’s Vue support, Flowbite’s vanilla JavaScript components and daisyUI’s CSS-only classes.

Legacy CSS needs a workaround. Adding Tailwind to an admin panel with high-specificity styles has two documented escape hatches, the important flag on the import and a class prefix such as tw. The first marks every utility !important, and the second lengthens every class name.

Long class lists bother some people more than others. Tailwind’s docs say duplication rarely bites because loops, multi-cursor editing and components remove it. Paired dark: classes lengthen the lists on a dashboard, and the docs recommend extracting a component for anything beyond a single element.

When data volume is the product, a component-based grid or charting suite is the better pick. Tailwind earns its place when the interface itself is the product.

Tailwind Dashboard FAQ

Can a v3 dashboard template be upgraded to v4?

Yes, in most cases. The upgrade tool migrates dependencies and the config file, and rewrites template classes.

A few v3 config options don’t carry over, namely corePlugins, safelist and separator. Safelisted classes move to the @source inline() directive.

Can Alpine.js run a sidebar toggle without a JavaScript framework?

Yes. An x-data block holds the open state, x-on:click flips it, and x-show or x-bind:class applies the sidebar classes.

The Persist plugin can also remember the collapsed state across page loads, because it saves values to localStorage.

Is Tailwind or Bootstrap the better base for a dashboard?

Bootstrap fits teams that want predesigned widgets out of the box. Tailwind is the better base for custom interfaces, and since it ships utility classes with no built-in UI components, kits such as daisyUI add component class names on top.

A full comparison of Tailwind and Bootstrap goes through the trade-offs.

Are Tailwind dashboards accessible by default?

No. Tailwind supplies styling utilities, so focus handling and ARIA behavior come from the components you choose.

In v4, outline-hidden keeps an invisible outline that still shows in forced colors mode, while the new outline-none removes it entirely. Run a web accessibility checklist against the finished interface.

Does a Tailwind dashboard work from the CDN build without a Node step?

Yes for a prototype, no for production. Tailwind’s docs say the Play CDN is designed for development only.

It loads the @tailwindcss/browser@4 script and accepts custom CSS in a style block typed text/tailwindcss. Production builds go through the Vite plugin, PostCSS or the Tailwind CLI.

Keeping Tailwind Dashboard Markup Maintainable as Screens Multiply

A Tailwind dashboard stays maintainable when each repeated class list gets one home, ordered by how far the repetition spreads.

Where one page already repeats the markup, loop it. Once a pattern crosses a file, extract a component or template partial. Single-element styles can move into an @layer components class.

The file boundary is the threshold. Tailwind’s docs treat multi-cursor editing as the fix for duplication inside one file, so a second file that copies a stat card is the signal to extract it.

Extraction has a trade-off: components should expose props instead of accepting outside classes. Conflicting utilities resolve by stylesheet order, not by position in the class attribute, so an overriding class can silently lose.

This sequence was checked against the Tailwind CSS v4.3 documentation in October 2026, and a new major version is the point to recheck it.