Skip to content

Dashboard Layouts

Six shells that cover almost every product screen. The layout is not the hard part — deciding what deserves the top-left is.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground

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.

OverviewLast 7 days
Revenue
$48.2k+12.4% increase
Active users
2,847+3.1% increase
Error rate
0.42%-18% decrease

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.

OverviewLast 7 days
Revenue
$48.2k+12.4% increase
Active users
2,847+3.1% increase
Error rate
0.42%-18% decrease

Requests per minute

All regions

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.

GH
Grace HopperCompiler Co
Proposal
Deal confidence72%

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.

Users4
IDEmailRoleStatus
usr_8241ada@analytical.coOwnerActive
usr_9930grace@compiler.coAdminActive
usr_1174alan@bletchley.ukMemberInvited
usr_5502kj@orbital.ioMemberSuspended

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.

Sprint 14ALGHAT
Backlog3
Rate limiting
Audit log export
SAML
In progress2
Billing webhooks
Search relevance
Review1
Dark theme audit
Done2
Session expiry
CSV import

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.

incidentsLive
GH
p95 is back under 300ms after the index change.
Confirmed on the dashboard. Closing the incident.
AT
I will write the postmortem this afternoon.

Settings — configuring

A narrow column against a section list. No Save button: each control commits on change, because people flip one switch and leave.

NotificationsChanges save as you make them.

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.

Primary
Secondary
Tertiary
Secondary
Detail
Detail
Tertiary
Detail
Rarely seen

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.

Loading
Errors
0.42%-18% decrease
Healthy
p95
1.4s+62% increase
Degraded
Collecting…
No data yet
4m old
Stale
Live
Live
Nothing to review
Empty queue
Revenue
$48.2k+12.4% increase
Compact

Anatomy

Every part, every measurement, and the reason it is that number.

OverviewLast 7 days
Revenue
$48.2k+12.4% increase
Active users
2,847+3.1% increase
Error rate
0.42%-18% decrease

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.

  1. 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.

  2. 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.

  3. Content gutter20–24px

    Matches the panel padding inside, so a card at the edge does not look inset twice.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. Row rhythm16px between blocks

    Larger than the gap inside the tile grid, so the grid reads as one block rather than as loose tiles.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed 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

TokenValueUsed for
sidebar widthPersistent navigation
bar heightContext bar
gutterContent inset
grid gapBetween tiles
block gapBetween sections

Radius

TokenValueUsed for
--radius-xl—Panels and tiles

Typography

TokenValueUsed for
--text-h2KPI values

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightPaddingLabel gapMin widthMax widthWhen to use
Sidebar———240px—Expanded. Label plus icon.
Sidebar, collapsed———64px—Icon only, with a tooltip. For screens people live in.
Context bar52–56px0 20px———Title, filters, primary action.
Content gutter—20–24px———Matches panel padding.
KPI tile96–112px16px———Label, value, delta, sparkline.
Tile grid——12px——Three or four per row. Six is the ceiling.
Chart panel240–320px————Tall enough to read a trend, short enough to keep the tiles visible.
Master list———20rem28remMaster–detail. Below 20rem the rows truncate.
Kanban column——12px17rem—Three to five columns. Eight is unreadable.
Settings column————34remOne column, same measure as a form.

Do

Primary
Secondary
Tertiary
Secondary
Detail
Detail
Tertiary
Detail
Rarely seen
Put the most actionable number top-leftThe eye lands there first and attention falls off down and right. Whatever sits there is what the product is claiming matters most.
Revenue
$48.2k+12.4% increase
vs last week
Give every number a comparisonA value on its own cannot be judged. "$48.2k" means nothing; "$48.2k, up 12.4% on last week" is a fact someone can act on.
Keep the navigation in one placeA persistent sidebar makes position into memory. Moving navigation between screens costs the user their map on every one.
Revenue
$48.2k
Revenue
$48.2k+12.4% increase
Offer a density toggleDaily users want more on screen and will learn it; occasional users want space and labels. Both are right, so ship both rather than picking.
Updated 4m ago
Say when the data is fromA dashboard with no timestamp is trusted until it is silently wrong. "Updated 4m ago" is the difference between a stale number and a lie.

Don't

Do not put nine tiles in the gridEqual size means equal importance. Nine equally important things is the same as none, and the user has to do the prioritising you skipped.
Do not make everything a chartA single number is a chart of one point with more ink. Use a chart when the shape over time is the information, and a number when it is not.
rows reorder mid-click → wrong record opened
Do not auto-refresh under the cursorA table that reorders while someone is reaching for a row makes them click the wrong thing. Refresh on a signal they control, or hold the update while they are interacting.
Lifetime page views
14,382,904
Do not show a metric nobody acts onTotal page views since launch is decoration. If no decision changes with the number, it is taking space from one that would.
Do not scatter navigation across shellsTabs here, a sidebar there, a menu on the third screen. Each one is defensible alone and together they mean the user has no map.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.3.1Info and RelationshipsA1.4.1Use of ColorA2.4.1Bypass BlocksA2.4.6Headings and LabelsAA4.1.3Status MessagesAA

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

TabMoves through the landmarks in order: navigation, then the bar, then the content.
Skip linkJumps 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 ⌘KFocuses 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.
AttributeApplied toNotes
<nav> <main> <aside>The shell regionsReal landmarks, so a screen reader can jump between them. This is what makes a dense screen navigable at all.
aria-live="polite"Metrics that updateOn the specific value, never the whole panel — a live region wrapping a grid re-reads all of it on every tick.
aria-labelEach panelPanels are sections. Naming them lets a screen-reader user skip to the one they want instead of hearing every tile.
Sparklinearia-hidden + text alternativeThe shape means nothing read aloud. Hide it and let the value and delta carry the meaning.
aria-currentThe selected row in a master listSelection drives the whole right-hand pane, so it has to be programmatically obvious which row it is.

Code

Example usage

tsx
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

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

PropTypeDefaultDescription
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.
deltanumber—Percentage change. Rendered with a sign, an arrow and a colour — three signals, never colour alone.
deltaLabelstring—What the delta is against. "vs last week" — without it the number is unjudgeable.
sparknumber[]—Sparkline data. Shape, not precision; it answers "is this normal?".

Panel

PropTypeDefaultDescription
titleReactNode—One idea per panel. If it needs a legend to explain itself, it is two panels.
descriptionReactNode—One line of context under the title.
actionsReactNode—Controls scoped to this panel only. Global controls belong in the context bar.
footerReactNode—A link to the full view, usually. Inset background.

Notes

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.