Skip to content
oks-ui
Page build

Build an Admin Dashboard in React

· 2 min read · oks-ui team

An admin dashboard with a sidebar, two stat cards and a bar chart, with a figure lifting off the corner.
ShareXLinkedIn
On this page
  1. Decide what goes on it first
  2. 1. KPI cards
  3. 2. The revenue chart
  4. 3. Recent orders
  5. 4. The layout
  6. 5. Loading states
  7. Dark mode

An admin dashboard is mostly three things: a row of numbers that matter, a chart that shows the trend, and a table of recent activity. In this tutorial we build all three with oks-ui and arrange them in a layout that works from phone to desktop.

import { Card } from "oks-ui/card";
import "oks-ui/card.css";
import { Stat } from "oks-ui/stat";
import "oks-ui/stat.css";
import { Chart } from "oks-ui/chart";
import "oks-ui/chart.css";
import { Table, type TableColumn } from "oks-ui/table";
import "oks-ui/table.css";
import { Chip } from "oks-ui/chip";
import "oks-ui/chip.css";

Decide what goes on it first

The hardest part of a dashboard isn't the code, it's choosing what to show. A useful test: every number on the first screen should answer a question someone actually asks every day. "How much did we sell this week?" earns a card. "Total registered users since launch" usually doesn't, because it only ever goes up and nobody acts on it.

Keep the top row to four numbers. Show each one next to its change over a comparable period, because a number without context can't tell you whether today is good or bad. Put detail — the chart and the table — underneath, where people look once the top row has told them something is worth a closer look.

1. KPI cards

Stat shows a label, a big value, and an optional change with a direction. The direction colours the change pill and picks the arrow.

const kpis = [
  { label: "Revenue", value: "$48,210", delta: "+12.4%", trend: "up" },
  { label: "Orders", value: "1,284", delta: "+3.1%", trend: "up" },
  { label: "Refunds", value: "18", delta: "-2", trend: "down" },
  { label: "Conversion", value: "3.2%", delta: "0.0%", trend: "flat" },
] as const;

<div className="kpi-grid">
  {kpis.map((k) => (
    <Card key={k.label} shadow="sm" radius="lg" style={{ padding: 20 }}>
      <Stat label={k.label} value={k.value} delta={k.delta} trend={k.trend} />
    </Card>
  ))}
</div>

Stat also takes an icon, a help line under the value, and a spark slot for a tiny chart.

Advertisement

2. The revenue chart

Chart takes rows of data, the key for the x-axis, and the series to draw. Multiple series get a legend automatically.

const revenue = [
  { month: "Jan", online: 12400, store: 8200 },
  { month: "Feb", online: 13900, store: 7900 },
  { month: "Mar", online: 15100, store: 8800 },
  { month: "Apr", online: 16800, store: 9100 },
  { month: "May", online: 18200, store: 9600 },
  { month: "Jun", online: 21000, store: 10400 },
];

<Card shadow="sm" radius="lg" style={{ padding: 20 }}>
  <Chart
    type="area"
    title="Revenue"
    description="Online vs. in-store, last 6 months"
    data={revenue}
    x="month"
    series={[
      { key: "online", name: "Online" },
      { key: "store", name: "In store" },
    ]}
    height={280}
  />
</Card>

Change type to "line" or "column" and nothing else needs to change. The chart has tooltips on hover and can be browsed with the arrow keys.

3. Recent orders

Table takes column definitions and rows. A column's render function controls how a cell looks — here, a status Chip.

type Order = { id: string; customer: string; total: number; status: "Paid" | "Pending" | "Refunded" };

const STATUS_COLOR = { Paid: "success", Pending: "warning", Refunded: "default" } as const;

const columns: TableColumn<Order>[] = [
  { key: "id", header: "Order" },
  { key: "customer", header: "Customer", sortable: true },
  {
    key: "total",
    header: "Total",
    align: "end",
    sortable: true,
    render: (o) => `$${o.total.toFixed(2)}`,
  },
  {
    key: "status",
    header: "Status",
    render: (o) => (
      <Chip size="sm" variant="soft" color={STATUS_COLOR[o.status]}>{o.status}</Chip>
    ),
  },
];

<Card shadow="sm" radius="lg" style={{ padding: 20 }}>
  <Table aria-label="Recent orders" columns={columns} rows={orders} getRowKey={(o) => o.id} />
</Card>

Sortable columns sort on click, with no extra code.

4. The layout

A few lines of CSS grid make it responsive: four KPI cards in a row on desktop, two on phones.

.dashboard { display: grid; gap: 16px; }
.kpi-grid { display: grid; gap: 16px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dashboard-main { display: grid; gap: 16px; grid-template-columns: 2fr 1fr; }

@media (max-width: 900px) {
  .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dashboard-main { grid-template-columns: 1fr; }
}

Put the chart in the wider column of .dashboard-main and a smaller card — top products, or a donut chart — in the narrow one.

5. Loading states

Dashboards load data, and blank cards while waiting look broken. Table shows skeleton rows with isLoading, and Chart shows a loading overlay with the same prop:

<Table aria-label="Recent orders" columns={columns} rows={orders ?? []} getRowKey={(o) => o.id} isLoading={!orders} />
<Chart type="area" data={revenue ?? []} x="month" series="online" isLoading={!revenue} />

Dark mode

Every component here uses oks-ui's design tokens, so the whole dashboard switches to dark mode when you set data-theme="dark" on the html element — cards, chart, table and chips included.

See a complete version on the patterns page.

Advertisement

Was this post helpful?

ShareXLinkedIn