Building the whole admin dashboard, not just this layout?
You just browsed 17 Tailwind dashboard layouts. Mosaic by Cruip is a complete Tailwind CSS admin dashboard template, coded in HTML, React, Next.js, Vue and Laravel, with a Figma version too. It costs $69.
A dark, Vercel-generation bento that now fills the viewport and packs perfectly: hero MRR tile at 2×2, a full-height activity rail, a 30-day uptime strip and a 12-week traffic chart, laid out with col-span-2 row-span-2 on a grid-cols-4 auto-rows-[minmax(8rem,1fr)] board. Hover a tile and one arbitrary :has() variant on the board dims its neighbours; the board re-packs to two columns with @max-[44rem]/page:.
The canonical SaaS shell: top bar, a sidebar that folds from 236px to a 68px icon rail, and a fluid main area. The fold is w-59 → group-data-collapsed/shell:w-17, labels fade with group-data-collapsed/shell:opacity-0, and rail tooltips are generated by after:content-[attr(data-label)]. The script sets one attribute — it never touches a class — and now remembers the choice and answers the <kbd>[</kbd> shortcut.
Four KPI cards that flow 4 → 2 → 1 columns from the width of their own container (@min-[34rem]/kpi:grid-cols-2 @min-[60rem]/kpi:grid-cols-4) — never the orphaned 3 + 1 row that auto-fit produces. New in the port: a working 7d / 28d / 90d range switch built from a radio group, where group-has-[#tdl03-7:checked]/kpi:inline swaps every value, delta and sparkline with zero JavaScript.
The inbox trinity — folder rail, scrollable conversation list, fluid detail pane — as one grid-cols-[13.5rem_20rem_minmax(0,1fr)] row. Selection is a native radio group: has-checked: highlights the row, group-has-[#tdl04-r2:checked]/split:flex swaps the pane, and the pane now fades in with starting:opacity-0. Arrow keys work out of the box, and the reply box is a real labelled textarea.
The app-shell scroll model in utilities: the frame never moves, the page scrollbar is dead, and the activity feed and deploy table scroll on their own with sticky, blurred section headers. It is still three load-bearing ideas — grid-rows-[3.5rem_minmax(0,1fr)], min-h-0 down the chain, overflow-y-auto overscroll-contain on the scroller — now each a class you can point at.
Tailwind has no grid-areas utility — and this demo shows you do not need one. The whole header / sidebar / main / aside / footer shell is an arbitrary property, [grid-template-areas:'header_header_header'_'sidebar_main_aside'_'footer_footer_footer'], re-drawn under @max-[41rem]/stage:. Drag the stage edge and a live badge reports which template is active.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
One markup, two behaviours, side by side: in a wide panel the sidebar sits in-flow; in the phone frame @max-[35rem]/app: turns it into an off-canvas drawer with a blurred click-to-close backdrop. One data-open attribute drives the slide (group-data-open/app:translate-x-0). The script now does what the CSS original only promised in a comment: inert on the closed drawer, focus moved in on open and restored on close, plus Esc and a close button.
The same widget markup in three slots — a narrow card, a medium card and one you drag — renders three layouts: stacked, value-beside-chart, and full analytics with a legend rail. In v4, container queries are built in: @container/w on the slot, @min-[21.25rem]/w: and @min-[32.5rem]/w: on the widget. A live badge on every card names the layout it is currently in.
Merchant-panel density with zero JavaScript: a stat strip, an orders table and a quick-action rail. Tick any order and a bulk-actions bar slides up via group-has-[tbody_input:checked]/card:translate-y-0 — and now it counts your selection with a CSS counter and the filter tabs really filter, each row hiding itself with group-has-[#tdl09-unpaid:checked]/card:hidden.
Dark mode as a data change, not a redesign. Components use semantic utilities only (bg-tdl10-card, text-tdl10-ink); @theme inline maps those to runtime variables; and the app root flips the variables with has-[#tdl10-dark:checked]:[--tdl10-bg:…]. New in the port: a Light / Dark / System segmented switch, where System follows the OS through the built-in dark: variant.
The cloud-console pattern: a dark icon rail, a secondary drawer whose links swap with the rail selection, and the work area. Rail selection is a radio group — group-has-[#tdl11-r2:checked]/console:flex swaps the sub-nav, the breadcrumb and the page heading — and a checkbox folds the drawer by animating grid-template-columns from 14.25rem to 0rem. Zero JavaScript.
A complete shell where the server template stays plain: <header>, <nav>, <main>, <footer>, lists and links — almost no classes inside. All styling hangs off one root element via child and descendant variants ([&>nav]:, [&_main_section]:, [&_a]:), so Django, Laravel, Rails or PHP partials render clean HTML and still get a modern Tailwind look.
Your data on the left, a resident AI copilot docked on the right. The panel folds by animating grid-cols-[minmax(0,1fr)_21.25rem] to group-data-closed/split:grid-cols-[minmax(0,1fr)_0rem], so the dashboard reflows in the same motion. New in the port: the copilot actually talks — the pending answer streams in word by word, suggestion chips and the composer send real messages, and <kbd>Esc</kbd> closes the panel.
The spreadsheet contract in utilities: a 10-column positions table scrolls both ways inside its panel while the header row pins to the top, the ticker column freezes to the left, and the corner cell wins both fights with a three-step z-10 / z-20 / z-30 ladder. New in the port: a sticky totals row pinned to the bottom with sticky bottom-0, and a Compact rows toggle that re-pads every cell through has-[#tdl14-dense:checked]:.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Eight numbered widgets, two packing engines, one switch. Engine A is columns-[3_15rem] + break-inside-avoid: true masonry, but it reads down each column. Engine B is grid-flow-dense auto-rows-2 with row-span-(--rows): left-to-right order with holes backfilled, heights quantised to an 8px beat. The numbers on the cards make the trade-off obvious in two seconds; the switch is a radio pair read by group-has-[#tdl15-grid:checked]/board:.
The NOC-wall contract: every pixel of the viewport, zero scrollbars. Status bar, KPI strip, chart grid and incident ticker are rows of grid-rows-[2.75rem_auto_minmax(0,1fr)_2.25rem] on an h-svh overflow-hidden frame, type rides clamp(), and gauges sweep in on a registered @property angle. New in the port: the clock actually ticks, and the ticker has a pause button (WCAG 2.2.2) driven by has-checked:.
The GitHub / Stripe school: no sidebar — a global top bar, a sticky tab strip with an animated underline, and content in a centred w-[min(100%_-_3rem,57.5rem)] column. Tabs are radios: group-has-[#tdl17-t2:checked]/page:grid swaps the panes and has-checked:after:scale-x-100 slides each underline in. New in the port: panes fade in with starting:, the tab strip scrolls horizontally on phones, and invoices and plans gained real status and “Current” affordances.
FAQ
Frequently asked questions
How do I build a collapsible sidebar admin dashboard in Tailwind?
Demo 02 is the canonical shell: a two-column frame where the sidebar track animates between a full 236px rail and a 68px icon-only rail, with the main area staying fluid and reclaiming the freed pixels. Put the transition on the sidebar column width at the shell level, not on every child — one animation runs cleanly instead of each item fighting its own timing. Keep the icon and the text label as separate children inside each link so the label can fade out while the icon stays centred when the rail narrows; never collapse the whole <a> element or you lose the click target and the tooltip anchor. Toggle the collapsed state by flipping one class on the outer grid with a button, and let the grid-template-columns value swap do the work. Because the main pane is just the second track, nothing inside it has to know the sidebar moved.
How do I make dashboard cards respond to their own width instead of the viewport?
Use container queries, which demos 03 and 08 are built on. Mark the wrapper as a query container with @container and give it a name, then size children with container variants like @min-[34rem]/kpi:grid-cols-4 instead of the viewport-based md: / lg: prefixes. In demo 03 the four KPI cards flow 4 to 2 to 1 columns purely from the width of their own grid, so the same component behaves correctly in a full-width page and in a narrow sidebar slot. Demo 08 proves the point by dropping one widget markup into three slots — narrow, medium, and a resizable one — and each renders a different layout at the same instant. The common mistake is forgetting to declare the container on the parent: the @min-[...] variants then silently never match, and the card falls back to its base layout with no error. Name your containers so nested queries resolve against the right ancestor.
Why does my full-screen dashboard still scroll after I set h-screen?
A single h-screen on the outer frame is not enough — demos 16 and 05 show the real contract. The frame must be h-screenandoverflow-hidden, every row inside it needs an explicit height or a min-h-0 so flex and grid children can shrink below their content, and only the one panel that is meant to scroll gets overflow-auto. The usual bug is a flex or grid child that refuses to shrink: by default it has min-height: auto, so its content forces the whole frame taller than the viewport and a page scrollbar appears. Add min-h-0 (and min-w-0 for horizontal cases) to that child. Demo 05 keeps the header and rails fixed while an inner activity feed scrolls independently; demo 16 kills every scrollbar for a wall display. If a stray bar persists, walk down the tree and find the row missing its min-h-0.
Tailwind has no grid-template-areas utility — how do I build a named-area dashboard layout?
You do not need one, which is the whole point of demo 06. Define a normal grid with explicit column and row tracks using grid-cols-[...] and grid-rows-[...] on the shell, then place each region with col-span / row-span or the explicit col-start / row-start line utilities. The header spans the full top row, the sidebar takes the first column down the remaining rows, and the main and aside fill what is left — all addressed by grid lines rather than named strings. The advantage over real grid-template-areas is that you can re-place a region at a breakpoint by changing only its start/span utilities, with no second ASCII-art template to keep in sync. If you genuinely want named areas, use an arbitrary property like [grid-template-areas:...], but for a fixed header-sidebar-main shell the line-based approach reads more clearly and survives responsive changes better.
How do I make a data table with both a sticky header and a sticky first column in Tailwind?
Demo 14 is the spreadsheet contract: the 10-column table scrolls both ways inside its panel while row one and column one stay pinned. Put overflow-auto on the panel wrapper, not the table. Give the header cells sticky top-0 and the first-column cells sticky left-0, and raise their z-index so they paint above the scrolling body — the top-left corner cell needs the highest z of all because it must sit above both the sticky row and the sticky column. The frequent bug is a transparent sticky cell: once the body scrolls under it you see both layers overlap, so every pinned cell needs a solid bg- utility matching the surface. Also give sticky cells a fixed width so the two axes agree on where the frozen column ends. sticky fails entirely if any ancestor has overflow: hidden on the wrong axis — the scroll container must be the direct overflow-auto parent.
How do I add a light / dark / system theme toggle to a Tailwind dashboard?
Demo 10 treats dark mode as a data change, not a redesign. Every component uses semantic utilities only — tokens like bg-tdl10-card and a matching text token — so one attribute flip restyles the whole shell with no per-component dark variants scattered through the markup. The three-way control writes the choice: Light and Dark set an explicit attribute on the root, while System removes it and lets a prefers-color-scheme media query decide. The classic bug is a flash of the wrong theme on load because the attribute is applied after paint; set it from a tiny inline script in the document head before the body renders, reading the stored preference first. Persist the selection so a reload keeps it, and make sure System actually clears the override rather than hard-coding a value — otherwise the OS switching between light and dark at dusk does nothing. Build on tokens and you never touch a component when the palette changes.
Is an off-canvas sidebar drawer keyboard accessible, and how do I make it so?
Demo 07 shows one markup with two behaviours — in a wide panel the sidebar sits in-flow, in the phone frame it slides in as an off-canvas drawer — but the slide alone is not accessibility. The toggle must be a real <button> with aria-expanded reflecting open state and aria-controls pointing at the drawer, not a bare <div> with a click handler. When open on a narrow screen the drawer should trap focus so Tab cycles inside it, Esc should close it and return focus to the trigger, and the dimmed backdrop should be clickable to dismiss. Hide the off-screen drawer from the tab order when closed — a drawer translated off-canvas but still focusable lets keyboard users tab into invisible links. If you drive the whole thing with a checkbox for the zero-JS version, pair it with a <label> that is itself focusable and announce the state, because a raw checkbox hack gives screen readers no open/closed context.
When should I use a top-navigation dashboard instead of a sidebar layout?
Reach for demo 17, the top-bar-with-tabs pattern, when your app is shallow — a handful of primary sections rather than a deep tree. The GitHub and Stripe school drops the sidebar entirely: a global top bar plus a sticky tab strip with an animated underline gives every horizontal pixel to content, which suits reading-heavy and table-heavy pages. Prefer a sidebar (demos 02 and 11) once navigation grows past roughly seven destinations or needs nested groups, because a tab strip wraps or scrolls badly beyond that and multi-level nesting has nowhere to live. The off-canvas approach in demo 07 is the bridge for phone widths regardless of which you pick. Also avoid top-nav when you need persistent context — filters, a folder rail, a conversation list like the master-detail inbox in demo 04 — since those want a column that stays put. Match the chrome to the depth of the information architecture, not to a visual preference.