Back to Heroui

How to Build a React Dashboard with HeroUI Pro

apps/docs/content/blog/en/build-react-dashboard-with-heroui.mdx

3.1.012.5 KB
Original Source

I've watched too many teams spend their first two sprints hand-rolling a sidebar, a responsive navbar, and a metrics card layout — only to rip it out later when the requirements get real. If you have HeroUI Pro, skip that phase entirely.

This post shows the Pro-first approach: start with AppLayout, Sidebar, Navbar, KPI, DataGrid, and chart components, then plug in your data. The dashboard template already handles the layout, responsive behavior, and mobile navigation.

Project Setup

Install HeroUI and HeroUI Pro packages:

bash
npm install @heroui/styles @heroui/react @heroui-pro/react

HeroUI Pro requires access to the Pro package registry. The dashboard template uses these style imports:

css
@import "@heroui/styles/css";
@import "@heroui-pro/react/css";

@source "../**/*.{ts,tsx}";

body {
  background-color: var(--background);
}

Use AppLayout for the Frame

The app frame should come from HeroUI Pro AppLayout, not a custom flex layout.

tsx
"use client";

import type {ReactNode} from "react";

import {AppLayout} from "@heroui-pro/react";
import {usePathname, useRouter} from "next/navigation";
import {useCallback} from "react";

import {DashboardNavbar} from "./dashboard-navbar";
import {DashboardSidebar} from "./dashboard-sidebar";

export function AppShell({children}: {children: ReactNode}) {
  const router = useRouter();
  const pathname = usePathname();
  const navigate = useCallback((href: string) => router.push(href), [router]);

  return (
    <AppLayout
      navbar={<DashboardNavbar title="Dashboard" />}
      navigate={navigate}
      sidebar={<DashboardSidebar pathname={pathname} />}
      sidebarCollapsible="offcanvas"
    >
      {children}
    </AppLayout>
  );
}

AppLayout handles the relationship between the sidebar, navbar, mobile menu, and content region. Keep it as the app shell and customize inside the slots.

Use Pro Sidebar and Navbar

HeroUI Pro ships a Sidebar compound component and a Navbar compound component. The dashboard template uses both.

tsx
"use client";

import {Bell, Magnifier} from "@gravity-ui/icons";
import {Button} from "@heroui/react";
import {AppLayout, Navbar, Sidebar} from "@heroui-pro/react";

export function DashboardNavbar({title}: {title: string}) {
  return (
    <Navbar maxWidth="full">
      <Navbar.Header>
        <AppLayout.MenuToggle />
        <Sidebar.Trigger />
        <h1 className="text-foreground truncate text-xl font-semibold">{title}</h1>
        <Navbar.Spacer />
        <Button isIconOnly aria-label="Search" size="sm" variant="tertiary">
          <Magnifier className="size-4" />
        </Button>
        <Button isIconOnly aria-label="Notifications" size="sm" variant="tertiary">
          <Bell className="size-4" />
        </Button>
      </Navbar.Header>
    </Navbar>
  );
}

For navigation, render Sidebar.MenuItem items instead of building custom active-link styles:

tsx
"use client";

import {ChartColumn, Gear, House, Receipt} from "@gravity-ui/icons";
import {Sidebar} from "@heroui-pro/react";

const items = [
  {href: "/", icon: House, label: "Dashboard"},
  {href: "/orders", icon: Receipt, label: "Orders"},
  {href: "/analytics", icon: ChartColumn, label: "Analytics"},
  {href: "/settings", icon: Gear, label: "Settings"},
];

export function DashboardSidebar({pathname}: {pathname: string}) {
  return (
    <>
      <Sidebar>
        <Sidebar.Content>
          <Sidebar.Group>
            <Sidebar.Menu aria-label="Dashboard navigation">
              {items.map((item) => {
                const Icon = item.icon;
                const isCurrent =
                  item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);

                return (
                  <Sidebar.MenuItem
                    key={item.href}
                    href={item.href}
                    id={item.href}
                    isCurrent={isCurrent}
                    textValue={item.label}
                  >
                    <Sidebar.MenuIcon>
                      <Icon className="size-4" />
                    </Sidebar.MenuIcon>
                    <Sidebar.MenuLabel>{item.label}</Sidebar.MenuLabel>
                  </Sidebar.MenuItem>
                );
              })}
            </Sidebar.Menu>
          </Sidebar.Group>
        </Sidebar.Content>
      </Sidebar>
      <Sidebar.Mobile></Sidebar.Mobile>
    </>
  );
}

The full Pro template also includes a header profile area, footer items, badges, and mobile content reuse.

Use KPI for Metrics

Do not rebuild metric cards with Card unless you need a one-off design. Use KPI from HeroUI Pro.

tsx
"use client";

import {KPI} from "@heroui-pro/react";

const chartData = [
  {x: 1, y: 12},
  {x: 2, y: 18},
  {x: 3, y: 16},
  {x: 4, y: 24},
];

export function KpiRow() {
  return (
    <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
      <KPI>
        <KPI.Header>
          <KPI.Title>Revenue</KPI.Title>
        </KPI.Header>
        <KPI.Content>
          <KPI.Value currency="USD" maximumFractionDigits={0} style="currency" value={45231} />
          <KPI.Trend trend="up">12.5%</KPI.Trend>
        </KPI.Content>
        <KPI.Chart color="var(--color-accent)" data={chartData} height={60} strokeWidth={1.5} />
      </KPI>
    </div>
  );
}

The dashboard template uses this same pattern for analytics KPIs and finance KPIs.

Use Pro Charts

HeroUI Pro chart components keep chart styling aligned with the rest of the design system.

tsx
"use client";

import {Card} from "@heroui/react";
import {LineChart, NumberValue, TrendChip} from "@heroui-pro/react";

const data = [
  {day: "Mon", sessions: 1800, users: 1200},
  {day: "Tue", sessions: 2400, users: 1700},
  {day: "Wed", sessions: 2200, users: 1600},
  {day: "Thu", sessions: 3100, users: 2100},
];

export function SessionsCard() {
  const total = data.reduce((sum, point) => sum + point.sessions, 0);

  return (
    <Card className="rounded-2xl">
      <Card.Header className="flex-row items-start justify-between gap-4">
        <div className="flex flex-col gap-1">
          <Card.Title className="text-base">Sessions over time</Card.Title>
          <div className="flex items-baseline gap-2">
            <NumberValue
              className="text-foreground text-2xl font-semibold tabular-nums"
              maximumFractionDigits={0}
              value={total}
            />
            <TrendChip trend="up">18.3%</TrendChip>
          </div>
          <span className="text-muted text-xs">vs. previous 30 days</span>
        </div>
      </Card.Header>
      <Card.Content>
        <LineChart data={data} height={240}>
          <LineChart.Grid vertical={false} />
          <LineChart.XAxis dataKey="day" minTickGap={32} tickMargin={8} />
          <LineChart.YAxis width={40} />
          <LineChart.Line
            dataKey="sessions"
            dot={false}
            name="Sessions"
            stroke="var(--chart-2)"
            strokeWidth={2}
            type="monotone"
          />
          <LineChart.Line
            dataKey="users"
            dot={false}
            name="Users"
            stroke="var(--chart-4)"
            strokeWidth={2}
            type="monotone"
          />
          <LineChart.Tooltip content={<LineChart.TooltipContent />} />
        </LineChart>
      </Card.Content>
    </Card>
  );
}

Use the chart components from Pro for dashboard analytics, then swap the sample arrays for your real metrics.

Use DataGrid for Tables

Dashboards usually need dense, sortable tables. Use Pro DataGrid instead of composing raw table markup.

tsx
"use client";

import type {DataGridColumn, DataGridSortDescriptor} from "@heroui-pro/react";

import {Avatar, SearchField} from "@heroui/react";
import {DataGrid} from "@heroui-pro/react";
import {useMemo, useState} from "react";

type Member = {
  id: string;
  name: string;
  email: string;
  avatar: string;
  role: string;
};

const members: Member[] = [
  {
    id: "1",
    name: "Kate Moore",
    email: "[email protected]",
    avatar: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue-light.jpg",
    role: "Admin",
  },
];

export function MembersTable() {
  const [search, setSearch] = useState("");
  const [sortDescriptor, setSortDescriptor] = useState<DataGridSortDescriptor>({
    column: "name",
    direction: "ascending",
  });

  const filteredMembers = useMemo(() => {
    if (!search) return members;
    const q = search.toLowerCase();

    return members.filter(
      (member) => member.name.toLowerCase().includes(q) || member.email.toLowerCase().includes(q),
    );
  }, [search]);

  const columns = useMemo<DataGridColumn<Member>[]>(
    () => [
      {
        accessorKey: "name",
        allowsSorting: true,
        cell: (item) => (
          <div className="flex items-center gap-3">
            <Avatar className="size-8">
              <Avatar.Image alt={item.name} src={item.avatar} />
              <Avatar.Fallback>{item.name[0]}</Avatar.Fallback>
            </Avatar>
            <div className="flex min-w-0 flex-col">
              <span className="text-xs font-medium">{item.name}</span>
              <span className="text-muted text-xs">{item.email}</span>
            </div>
          </div>
        ),
        header: "Member",
        id: "name",
        isRowHeader: true,
        minWidth: 220,
      },
      {
        accessorKey: "role",
        allowsSorting: true,
        header: "Role",
        id: "role",
        minWidth: 160,
      },
    ],
    [],
  );

  return (
    <div className="flex flex-col gap-4">
      <SearchField className="w-full sm:w-[220px]" name="member-search" onChange={setSearch}>
        <SearchField.Group>
          <SearchField.SearchIcon />
          <SearchField.Input placeholder="Search..." />
          <SearchField.ClearButton />
        </SearchField.Group>
      </SearchField>

      <DataGrid
        aria-label="Members"
        columns={columns}
        contentClassName="min-w-[520px]"
        data={filteredMembers}
        getRowId={(item) => item.id}
        sortDescriptor={sortDescriptor}
        onSortChange={setSortDescriptor}
      />
    </div>
  );
}

This matches the template's table direction: typed columns, a controlled sort descriptor, search outside the grid, and getRowId for stable row identity.

Compose the Dashboard Page

The page should mostly assemble widgets. In the Pro template, the dashboard view looks like this:

tsx
"use client";

import {DashboardToolbar} from "../widgets/dashboard-toolbar";
import {EmployeesTable} from "../widgets/employees-table";
import {KpiRow} from "../widgets/kpi-row";
import {SalesPerformanceCard} from "../widgets/sales-performance-card";
import {TrafficSourceCard} from "../widgets/traffic-source-card";

export function DashboardPage() {
  return (
    <div className="mx-auto flex max-w-7xl flex-col gap-4 px-5 pb-10 pt-4">
      <DashboardToolbar />
      <KpiRow />
      <div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
        <SalesPerformanceCard />
        <TrafficSourceCard />
      </div>
      <EmployeesTable />
    </div>
  );
}

That is the main architectural point: the page should read like product composition, not infrastructure.

Why HeroUI for Dashboards

Dashboards are dense — data tables, form controls, navigation, and overlays on a single screen. HeroUI handles the accessibility and styling for all of those pieces so you can focus on your data layer.

  • Data tables that handle accessibility for you. Sortable columns, row selection, keyboard navigation, and screen reader announcements come from React Aria. You don't reimplement them per table.
  • Static CSS for data-heavy pages. Tailwind v4 generates all styles at build time. Dashboards with KPI cards, charts, and grids don't pay a runtime styling tax.
  • Pro primitives instead of hand-rolled infrastructure. AppLayout, Sidebar, Navbar, KPI, DataGrid, and chart components from HeroUI Pro save weeks of dashboard scaffold work.
  • AI assistants that know the Pro API. The MCP server and agent skills let AI tools scaffold dashboard views with correct component APIs instead of guessing.

Next Steps

  • Start from the HeroUI Pro dashboard template if you have Pro access.
  • Replace the mock files in src/data/* with your API, database, or analytics source.
  • Keep AppLayout, Sidebar, Navbar, KPI, DataGrid, and Pro charts as the foundation.
  • Use base HeroUI components for local UI that does not need a Pro primitive.