Skip to content
Sudip KC writing notes in a journal at a desk
SK.
← All articles

UI/UX · · 7 min read

How I Design Dashboards for Complex Applications

  • UI/UX
  • Figma
  • Design Systems
  • React
  • Case Study

Most dashboards fail the same way: they show everything the database knows and call it insight. The ones that work start from a much smaller question. What does this person need to decide or do in the next five minutes?

I've designed dashboards for a membership platform, for restaurant operations in NovaRestro, and for internal admin tools. They all started out too busy. Each time, the work that made them useful was mostly removal. This is the process I've settled on, from first questions in Figma to the React components that ship.

Start with roles, not widgets

The first mistake I made on early dashboards was designing one screen for "the user." Complex applications don't have one user.

In NovaRestro, the owner, a manager, a cashier, a waiter and the kitchen all log into the same system. The owner wants to know if today is going well and whether stock is about to run out. The cashier wants open bills. The kitchen wants a queue of tickets, big enough to read from across a hot room. A single dashboard trying to serve all of them serves none of them.

So before any layout, I write one line per role:

  • Owner: Is the business healthy today, and is anything going wrong?
  • Manager: What needs my attention right now on the floor?
  • Cashier: Which tables are ready to pay, and is the drawer balanced?
  • Kitchen: What do I cook next, and what's late?

Those lines become the brief for each view. If a widget doesn't help answer the role's question, it doesn't go on that role's dashboard. This lines up neatly with permissions too; I wrote about designing RBAC for 15+ roles, and the dashboard is often where those roles become most visible.

Separate monitoring from doing

There are two kinds of dashboard and they want different designs.

A monitoring dashboard is glanced at. It needs a few numbers, clear trends and obvious alerts. Think of the owner checking their phone between meetings.

An operational dashboard is worked in. The kitchen display, a cashier's open bills, an admin's approval queue. These need big tap targets, fast actions and very little decoration. Charts are usually noise here.

Mixing the two is where clutter comes from. A cashier doesn't need a revenue chart sitting above their open bills. An owner doesn't need a list of every ticket. I decide early which kind each screen is, and design for that.

The layout hierarchy I use

For monitoring dashboards, I follow roughly the same order from top to bottom:

  1. Alerts. Only when something is wrong. Low stock, a failed sync, an unpaid bill older than an hour. If there's nothing wrong, this area disappears.
  2. Key numbers. Three to five, no more. Each with a comparison so the number means something: today vs. the same day last week, for example.
  3. Trends. One or two charts that show direction over time.
  4. Lists to act on. Recent orders, pending approvals, items to restock.
  5. Everything else. Behind a link to a reports page.
If everything on the dashboard is important, nothing is. The top of the screen should change depending on what's actually happening.

The alerts slot is the one I fight for in every project. A dashboard that looks identical on a good day and a bad day isn't doing its job.

Numbers need context

A card that says "Orders: 142" tells you almost nothing. Is that good? Compared to what? Since when?

Every key number I design has three parts:

  • The value, large and readable.
  • A comparison, like "+12% vs last Saturday," with color only as a secondary signal.
  • A time frame, stated clearly, so nobody wonders if it's today, this week or all time.

I'm careful with color. Green and red are fine as a hint, but the arrow or the plus/minus sign has to carry the meaning on its own. Color-blind users and bright sunlight both make color unreliable. My time on accessible palette work at Tonalist made me much stricter about this, and I check contrast against WCAG AA on every dashboard now.

Charts: fewer, simpler, labelled

I default to the most boring chart that answers the question. Line charts for trends over time. Bar charts for comparing categories. Almost never pie charts, and never 3D anything.

A few rules I keep:

  • Label directly. Put the series name at the end of the line instead of in a legend people have to decode.
  • Start bar charts at zero. Otherwise small differences look dramatic.
  • Show the empty case. A new restaurant has no history. "Not enough data yet, check back after a week of orders" beats an empty axis.
  • Don't chart what a number can say. If the insight is "sales are up 12%," a card does that better than a chart.

Design every state, not just the happy one

Static Figma mockups always show a dashboard with perfect data. Real dashboards spend a lot of time in other states, and those are where users lose trust.

For each widget I design:

  • Loading: skeletons that match the final layout, so nothing jumps.
  • Empty: a short explanation and, where it makes sense, the action that fills it.
  • Error: what failed, scoped to that widget, with a retry. One failed chart should never blank the whole page.
  • Stale: when data might be outdated, especially offline, a clear "last updated" time.
  • Overflow: what happens with 500 rows instead of 5, or a menu item with a very long name.

On the React side, this maps neatly onto TanStack Query. Each widget owns its query, so each one loads, fails and retries independently.

function LowStockCard() {
  const { data, isPending, isError, refetch } = useQuery({
    queryKey: ['inventory', 'low-stock'],
    queryFn: fetchLowStock,
  })

  if (isPending) return <CardSkeleton rows={3} />
  if (isError) return <CardError title="Low stock" onRetry={refetch} />
  if (data.length === 0) return <CardEmpty title="Low stock" message="Everything is stocked." />

  return <LowStockList items={data} />
}

That pattern matters more than any visual choice. A dashboard where one slow endpoint holds the entire page hostage feels broken even when the data is fine.

Tables are the real workhorse

In complex apps, most of the actual work happens in tables. They deserve as much design attention as the charts.

What I build into tables by default:

  • Sensible default sort. Usually newest first, or most urgent first.
  • Filters that stay visible. Show active filters as chips so people know why they're seeing what they see.
  • Filters in the URL. So a manager can share a filtered view or come back to it.
  • Row actions where the eye already is. Not hidden in a three-dot menu unless there are many.
  • A mobile plan. Tables rarely survive a phone screen. I usually switch to stacked cards with the two or three most important fields.

From Figma to components

I build dashboards from a small set of components that I design first in Figma and then mirror in React: a stat card, a chart card, a list card, a table, and the loading, empty and error versions of each. Same names in both places.

This is the same approach I used for Membersathi at GETS, where I designed 20+ screens and built 10+ reusable React components. Once the building blocks exist, new dashboards become mostly arrangement, and they stay consistent without anyone policing it. I go deeper into that handoff in my Figma to production workflow.

Test with the real person in the real place

The last step is the one I skip at my own risk. Watch the actual user use it, ideally where they work. A kitchen display that looks great on my monitor might be unreadable from two meters away with steam in the air. A manager's dashboard might be used mostly on a phone, one-handed, while walking between tables.

You learn more from ten minutes of watching than from a week of guessing.

The short version

Design per role. Decide whether each screen is for monitoring or for doing. Put alerts first and make them disappear when nothing's wrong. Give every number a comparison and a time frame. Design loading, empty, error and stale states for every widget, and let each widget fail on its own. Then go watch someone use it, and remove whatever they ignore.