← All writing

/ 3 min read

A design system for products people use all day

Marketing design systems are about brand. Enterprise ones are about density, keyboard use, states and consistency across many teams. What I put in a shared design system for data heavy panels, and what I leave out.

  • Design systems
  • React
  • Accessibility

I built and maintain the shared design system for the panels at the company I work for: supervision dashboards, reporting tools, configuration screens and editors used by banks and operators. These are products people keep open all day, and that changes what a design system needs to be good at.

A marketing site needs a few beautiful components. An enterprise product needs hundreds of screens to feel like one product, stay usable with a lot of data, and keep working when five teams build on them at once.

Tokens before components

Everything starts with design tokens: colour, spacing, type, radius, shadow and motion, named by role rather than by value. A button does not use "blue 600"; it uses accent.fill. That indirection is what makes themes, dark mode, high contrast and a later rebrand a change in one file instead of a hunt through every component.

:root {
  --surface: #ffffff;
  --surface-raised: #f6f7f9;
  --text: #14161a;
  --text-muted: #5b6270;
  --line: #e3e6eb;
  --accent-fill: #2f5fe0;
  --danger-fill: #c8352b;
  --space-1: 4px;
  --space-2: 8px;
  --row-height: 36px;
}
[data-density='compact'] {
  --row-height: 28px;
}

Density deserves its own token. People who process hundreds of rows a day want compact tables; occasional users want room to breathe. A single density setting that adjusts row heights and paddings everywhere serves both without two sets of components.

States are the real work

A component in a design tool usually has one state. In production it has many: loading, empty, partially loaded, error, read only, disabled with a reason, too much content, and right to left text. Enterprise screens spend most of their life in these states, so each component in the system ships with all of them designed and built, not left to the team that happens to use it.

  • Empty states say what to do next, not just "no data", with the action that fills the screen.
  • Disabled controls explain why, in a tooltip, because a greyed out button with no reason generates support tickets.
  • Loading keeps the layout, with skeletons the size of the content, so nothing jumps when data arrives.
  • Errors stay local. A failed widget shows its own error and a retry, and the rest of the dashboard keeps working.

Keyboard and accessibility are not extras

Heavy users are keyboard users. Tables move focus with the arrow keys, dialogs trap focus and return it when they close, menus open with Enter and close with Escape, and every icon button has a label. Building this into the shared components once means every team gets it for free, and nobody has to remember the details of focus management on a deadline.

It is also simply required. Banks and public institutions increasingly ask for accessibility compliance, and retrofitting it screen by screen is far more expensive than getting the primitives right.

Composable, not configurable

The fastest way to ruin a component library is the mega component: a table with forty props that tries to cover every case. Every new requirement adds a prop, the props interact in ways nobody tested, and eventually teams fork it.

I prefer small parts that compose. A data table is a set of pieces, header, row, cell, selection, sorting and virtualised body, that a screen assembles. The common case is wrapped in a ready made preset, and the unusual case uses the same parts directly instead of waiting for a new prop.

<DataTable rows={rows} getRowId={(r) => r.id}>
  <DataTable.Toolbar>
    <DataTable.Search />
    <DataTable.Filters schema={filters} />
    <DataTable.Export />
  </DataTable.Toolbar>
  <DataTable.Columns columns={columns} />
  <DataTable.VirtualBody />
  <DataTable.Footer />
</DataTable>

Adoption is a product problem

A design system nobody uses is just a folder. What made ours stick was treating the other teams as customers: documentation with real examples from our own products, a changelog written for humans, migration notes for every breaking change, and quick answers when someone was blocked. When a team needed something the system did not have, we built it with them and then moved it into the system, so the library grew from real needs rather than guesses.

The measure of success is not the number of components. It is that a new screen built by any team looks and behaves like the rest of the product on the first try, and that people using the product all day never have to relearn how something works.

Next article ↗

Who reported to whom last March? Modelling organisations over time