Dashboard Layouts
Six shells that cover almost every product screen. The layout is not the hard part — deciding what deserves the top-left is.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
Push the tile count to nine. Nothing gets bigger, nothing gets smaller — everything simply becomes equally unimportant, which is the failure mode of most real dashboards.
Requests per minute
All regions
Analytics — monitoring
KPI tiles above a chart. The user is asking "is anything wrong?", so the tiles carry a delta and a sparkline: the number alone cannot answer that.
CRM — working a queue
Master–detail. The list stays put while the detail changes, so the user never loses their place in a queue they are working through one item at a time.
Admin — managing records
A table and a filter bar, and very little else. Admin screens are found by search and used by people who know exactly what they came for.
Kanban — moving work through stages
Columns are the state. The layout is the data model, which is why a board with eight columns is unreadable — nobody holds eight states in mind.
Chat — conversation
The only shell anchored to the bottom, because the newest message is the important one. The composer is pinned and the transcript scrolls above it.
Settings — configuring
A narrow column against a section list. No Save button: each control commits on change, because people flip one switch and leave.
Where attention actually goes
Attention falls off down and to the right. This is the map you are laying out against, whether or not you designed for it.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Every part, every measurement, and the reason it is that number.
Requests per minute
All regions
The analytics shell. Sidebar, context bar, content well — and inside the well, tiles above a chart, in descending order of how quickly someone needs the answer.
- Sidebar240px, or 64px collapsed
Persistent, so position becomes memory. It answers "where am I" as well as "where can I go", which is why a dashboard uses it rather than a top-level menu.
- Context bar52–56px
What this screen is, and the controls that change what it shows — a date range, a filter, a search. Not global navigation, which lives in the sidebar.
- Content gutter20–24px
Matches the panel padding inside, so a card at the edge does not look inset twice.
- KPI grid3–6 tiles, 12px gap
Three to a row reads as a group; six reads as a wall. Past six, delete rather than shrink.
- Tilelabel 12px, value 28px
The number is the largest thing and the label the smallest, so the eye lands on the value and only then learns what it measures.
- Deltasign + arrow + colour
Three signals, because a bare number cannot say whether it is good. Never colour alone — down is good for error rate and bad for revenue.
- Sparkline~32px tall
Shape, not precision. It exists to answer "is this normal?", which a single number cannot, and it needs no axes to do it.
- Panelheader + body
One idea per panel with a title that says what it is. A panel needing a legend to explain its own contents is two panels.
- Row rhythm16px between blocks
Larger than the gap inside the tile grid, so the grid reads as one block rather than as loose tiles.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Color
| Token | Value | Used for |
|---|---|---|
| --ds-canvas | — | The well behind the panels |
| --ds-surface | — | Panels and tiles |
| --ds-border-subtle | — | Panel edges and rules |
| --ds-success-text | — | Positive delta |
| --ds-danger-text | — | Negative delta |
Spacing
| Token | Value | Used for |
|---|---|---|
| sidebar width | Persistent navigation | |
| bar height | Context bar | |
| gutter | Content inset | |
| grid gap | Between tiles | |
| block gap | Between sections |
Radius
| Token | Value | Used for |
|---|---|---|
| --radius-xl | — | Panels and tiles |
Typography
| Token | Value | Used for |
|---|---|---|
| --text-h2 | KPI values |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Height | Padding | Label gap | Min width | Max width | When to use |
|---|---|---|---|---|---|---|
| Sidebar | — | — | — | 240px | — | Expanded. Label plus icon. |
| Sidebar, collapsed | — | — | — | 64px | — | Icon only, with a tooltip. For screens people live in. |
| Context bar | 52–56px | 0 20px | — | — | — | Title, filters, primary action. |
| Content gutter | — | 20–24px | — | — | — | Matches panel padding. |
| KPI tile | 96–112px | 16px | — | — | — | Label, value, delta, sparkline. |
| Tile grid | — | — | 12px | — | — | Three or four per row. Six is the ceiling. |
| Chart panel | 240–320px | — | — | — | — | Tall enough to read a trend, short enough to keep the tiles visible. |
| Master list | — | — | — | 20rem | 28rem | Master–detail. Below 20rem the rows truncate. |
| Kanban column | — | — | 12px | 17rem | — | Three to five columns. Eight is unreadable. |
| Settings column | — | — | — | — | 34rem | One column, same measure as a form. |
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- Deltas must never rely on colour alone. Ours carries an arrow, a sign and a word, so red-green colour blindness — around one man in twelve — does not lose the meaning.
- Sparklines and chart fills are decorative and exempt, but any line carrying meaning on its own needs 3:1 against its background.
- Compact density shrinks padding, never type. A 10px label to save four pixels of height fails contrast at a glance even when it passes a ratio check.
Keyboard
| Tab | Moves through the landmarks in order: navigation, then the bar, then the content. |
| Skip link | Jumps past the sidebar to the content. Without it, every screen starts with the same twelve links. |
| ↑ ↓ | Moves through a master list. Selection updates the detail without leaving the list. |
| / or ⌘K | Focuses search or opens the command palette — the fastest route on a screen this dense. |
Screen readers
- A tile announces as "Revenue, $48.2 thousand, up 12.4 percent versus last week". The delta must be in the text, not only in the colour of an arrow.
- Give every panel a heading and keep the heading levels in order. A dashboard is where heading structure most often collapses into a dozen unrelated h3s.
- Announce refreshes politely and specifically. "Revenue updated" beats re-reading the entire grid on a thirty-second interval.
- A skip link past the sidebar is not optional here — the alternative is Tabbing through the same navigation on every screen.
Focus & touch
- Selecting a row in a master–detail layout must not move focus into the detail pane — the user is working down a list and needs the arrow keys to keep working. Focus moves only when they act on the detail deliberately. Never let a background refresh steal or reset focus.
- These shells are desktop-first by definition. Below the tablet breakpoint the sidebar becomes a drawer, tiles go to one column, master–detail becomes list-then-page, and a kanban board becomes one column with a stage picker — not a horizontally scrolling board nobody can use one-handed.
| Attribute | Applied to | Notes |
|---|---|---|
| <nav> <main> <aside> | The shell regions | Real landmarks, so a screen reader can jump between them. This is what makes a dense screen navigable at all. |
| aria-live="polite" | Metrics that update | On the specific value, never the whole panel — a live region wrapping a grid re-reads all of it on every tick. |
| aria-label | Each panel | Panels are sections. Naming them lets a screen-reader user skip to the one they want instead of hearing every tile. |
| Sparkline | aria-hidden + text alternative | The shape means nothing read aloud. Hide it and let the value and delta carry the meaning. |
| aria-current | The selected row in a master list | Selection drives the whole right-hand pane, so it has to be programmatically obvious which row it is. |
Example usage
1// Every shell here is the same three regions. Build it once.2function DashboardShell({ nav, bar, children }: ShellProps) {3 return (4 <div className="flex h-dvh overflow-hidden">5 <nav aria-label="Primary" className="w-60 shrink-0 border-r">{nav}</nav>6 <div className="flex min-w-0 flex-1 flex-col">7 <header className="h-14 shrink-0 border-b">{bar}</header>8 {/* min-h-0 is what lets the well scroll instead of the page */}9 <main className="min-h-0 flex-1 overflow-y-auto p-6">{children}</main>10 </div>11 </div>12 )13}1415// Analytics: tiles above a chart, in descending order of urgency16<DashboardShell nav={<Nav />} bar={<DateRange />}>17 <section aria-label="Key metrics" className="grid grid-cols-3 gap-3">18 <Stat label="Revenue" value="$48.2k" delta={12.4} spark={revenue} />19 <Stat label="Active users" value="2,847" delta={3.1} spark={users} />20 <Stat label="Error rate" value="0.42%" delta={-18} spark={errors} />21 </section>2223 <Panel title="Requests per minute" description="All regions">24 <Chart data={rpm} />25 </Panel>26</DashboardShell>2728// Master–detail: selection must NOT move focus into the detail pane,29// or arrow-keying down a queue stops working after the first row.30<div className="flex gap-4">31 <ul role="listbox" aria-label="Leads" onKeyDown={arrowKeys}>32 {leads.map((l) => (33 <li key={l.id} role="option" aria-selected={l.id === selected}>…</li>34 ))}35 </ul>36 <section aria-label="Lead detail">{detail}</section>37</div>CSS
.ds-shell {
display: flex;
block-size: 100dvh;
overflow: hidden; /* the well scrolls, never the page */
}
.ds-shell__nav { inline-size: 240px; flex: none; }
.ds-shell__nav[data-collapsed] { inline-size: 64px; }
.ds-shell__main {
display: flex;
flex-direction: column;
min-inline-size: 0; /* without this, a wide table pushes the
sidebar off screen instead of scrolling */
flex: 1;
}
.ds-shell__well {
min-block-size: 0; /* the flex-child rule that actually makes
overflow-y work inside a column */
flex: 1;
overflow-y: auto;
padding: 24px;
}
/* Tiles auto-fit, so the grid degrades by wrapping rather than by
squeezing six tiles into an unreadable row. */
.ds-stat-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
/* Density is a data attribute on the shell, not a separate stylesheet. */
[data-density='compact'] .ds-stat { padding: 10px; }
[data-density='compact'] .ds-table td { padding-block: 4px; }
@media (max-width: 1023px) {
.ds-shell__nav { display: none; } /* becomes a drawer */
.ds-stat-grid { grid-template-columns: 1fr; }
}Component API
Stat
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | string | — | What the number measures. The smallest text in the tile. |
| value* | ReactNode | — | The number. Largest thing in the tile, tabular figures so digits do not shift on update. |
| delta | number | — | Percentage change. Rendered with a sign, an arrow and a colour — three signals, never colour alone. |
| deltaLabel | string | — | What the delta is against. "vs last week" — without it the number is unjudgeable. |
| spark | number[] | — | Sparkline data. Shape, not precision; it answers "is this normal?". |
Panel
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | One idea per panel. If it needs a legend to explain itself, it is two panels. |
| description | ReactNode | — | One line of context under the title. |
| actions | ReactNode | — | Controls scoped to this panel only. Global controls belong in the context bar. |
| footer | ReactNode | — | A link to the full view, usually. Inset background. |
Professional tips
- Write the question each panel answers before you build it. A panel with no question is decoration, and this is the cheapest time to find that out.
- Order the tiles by how quickly someone needs the answer, not by category. Errors before revenue on an operations dashboard; the reverse on a business one.
- Make every tile a link to its own detail view. A number that raises a question and offers no way to pursue it is a dead end.
- Ship an empty state for every panel. New accounts see them all at once, and "No data yet" beats a chart of zeroes.
- One primary action per screen, in the context bar. Two competing buttons at the top of a dashboard is a decision nobody asked for.
- If the shell needs a horizontal scrollbar on a laptop, the layout has already failed — that is the width most of your users have.
Performance
- Virtualise any list past a few hundred rows. A CRM queue and an admin table both reach that quickly, and both feel broken before they look broken.
- Stagger polling across panels. Twelve panels refreshing on the same interval produces a synchronised stall every thirty seconds.
- Pause background refresh when the tab is hidden. A dashboard left open on a second monitor otherwise polls all day for nobody.
- Render tiles from cached values first and reconcile when the fetch lands. Perceived speed on this screen matters more than freshness by a second.
- Charts are the heaviest thing here. Load them below the tiles and let the numbers paint first.
Common mistakes
- Nine equally sized tiles, so nothing is primary.
- Numbers with no comparison, which cannot be judged.
- Colour-only deltas, invisible to a significant share of users.
- Auto-refresh that reorders rows under the cursor.
- A different navigation pattern on every screen.
- No timestamp, so stale data is trusted until it is embarrassing.
- Charts where a single number would do, and single numbers where the trend was the point.
- A kanban board with eight columns, which nobody can hold in mind.
Real-world recommendations
- Ask which decision each panel supports. The ones with no answer are usually the ones added because the data happened to be available.
- Watch what people actually click. Dashboards accumulate panels for years and almost never lose one; the click data is the only honest argument for deletion.
- The best dashboards are boring. If it is interesting, it is usually because something is wrong — which is exactly when it should be.
- Density complaints are audience complaints in disguise. The people asking for more on screen and the people asking for less are different people, and a toggle settles it.
- Master–detail beats a table with a modal almost every time. The user keeps their place in the queue, which is the entire job on a screen someone works through all day.