App screens
Analytics dashboard
A complete app shell, not just a card grid: a collapsible sidebar with sectioned navigation (Nav) and an account menu (Dropdown), a header with search and a user menu, a merged balance-plus-wallet card beside two stat cards, an earnings chart, savings-goal progress bars, and a transactions table — every one of them a real oks-ui component wired together, not a static mockup.
How it's built
The sidebar is a real `Nav` — `items` is a tree of `{ key, label, icon, badge, isSection, children }`, `selectedKey`/`onItemSelect` control the active row, and `isCollapsed` switches the whole thing to an icon-only rail. The small dot beside the active label is real too, not CSS on `[data-active]` — `Nav`'s `renderItem` prop hands back `{ isActive, content, props }` for full control per row.
`Nav`'s own CSS sets a fixed `width: 15rem` on itself regardless of its container (`--oks-nav-width`) — inside a flex column narrower than that, it silently overflows the sidebar sideways. Fixed the same way as any other width override in this project: `classNames={{ base: "w-full" }}`, which wins over the library's own `:where()`-scoped rule on specificity alone.
This is a real app shell, not a page that happens to have a sidebar: the outer wrapper has a bounded height (`h-[46rem]`) and `overflow-hidden`, the sidebar and header get their own `overflow-y-auto`/`shrink-0`, and only the middle content column scrolls. Without that, the whole thing — sidebar included — would scroll away as one long document, defeating the point of persistent nav. The sidebar-collapse toggle lives in the header (before the search field), not inside the sidebar itself.
Settings / Help desk / Log out, and every kebab ("⋯") menu, and the currency selector are all real `Dropdown`s — none of them were wired up in an earlier pass, which meant clicking them did nothing. `Dropdown`'s own wrapper is `display: inline-block` by default (shrink-to-fit) — `classNames={{ content: "block w-full" }}` plus `className="w-full"` on `Dropdown` itself is what makes the account-menu trigger actually span the sidebar's full width instead of hugging its own content.
Account balance and My wallet are **one** card, not two separate ones stacked in different grid rows — `@3xl:row-span-2` on that single card is what lets it sit beside Total expenses/Total savings (row 1) and Overview (row 2). The two subsections are separated by spacing alone, deliberately with no `border-t` rule between them — a hard divider would visually re-split what's meant to read as one continuous section.
Send money / Request money use `flex-1` on both buttons for a genuine 50/50 split, instead of each button sizing to its own label's width and ending up visibly uneven.
The account avatar is the user's own photo (`src=`), and the sidebar/header logo is the real `/logo.svg` static asset — this is the maintainer's own branded copy of the pattern, not stock photography or a fictional placeholder person.
The header search is a real `FormFieldSet type="search"` with `startIcon`/`endIcon` (two separate `<kbd>` chips), not a styled `<input>` — `classNames={{ control: "border-transparent bg-paper-sunken" }}` swaps its default bordered box for a filled, borderless one.
Every delta ("+4.8%", "-1.6%") is a real `Chip` with a directional arrow icon in `endContent` — a shared `DeltaChip` helper keeps the up/down colour and icon in one place.
The overflow menus are a shared `MoreButton` — a real `Dropdown` behind a `Button isIconOnly variant="ghost"` (no border, matching the reference's minimal treatment) with a few generic actions, reused on both stat cards, each wallet card, the savings plan, the overview chart, and each transaction row.
Account balance / Total expenses / Total savings sit in one row (`grid-cols-[1.25fr_1fr_1fr]`) with `items-start`, not the grid default `align-items: stretch` — stretching would force the shorter stat cards (or the merged card) to match a sibling's height instead of each keeping its own natural size, which is exactly what caused dead space in an earlier pass.
Wallet status ("Active"/"Inactive") is plain coloured text through `--oks-color-<role>-600`, not a filled `Chip` — a deliberately lighter treatment than the `Chip` used for transaction status.
Every icon badge is a real `Avatar` with `icon=`, coloured via the `color` prop — each transaction row uses a different semantic colour so the list reads as varied activity, not one repeated icon.
Every heading is a real `PageTitle`, always with `classNames={{ base: "flex-col items-start" }}` — `PageTitle`'s own wrapper is `display: inline-flex` with no set direction, built for a compact inline icon+title, not a stacked heading.
The sidebar is hidden below the `@3xl` container width so the pattern still reads correctly scaled down inside a gallery-card thumbnail, rather than squeezing a 240px rail into a few dozen pixels.
`Table`'s `classNames={{ cell: "whitespace-nowrap" }}` keeps every cell on one line — without it, "Software license" or a date wraps across two or three lines the moment the column narrows. `Table`'s own container already has `overflow: auto` built in, so once cells can't wrap, a table wider than its card scrolls horizontally instead — the fix is stopping the wrap, not adding scroll that was already there.
My savings plan carries four goals, each with its own semantic `color` driving both the `Avatar` icon badge and the `Progress` bar — varied colour reads as varied activity, and keeps the card from looking sparse beside the taller Recent transactions table.
Every direct child of the scrollable content body carries `shrink-0`. A flex child defaults to `flex-shrink: 1`, so without it the browser silently compresses these sections to fit the available space instead of letting them overflow — and the parent's `overflow-y-auto` never has anything to scroll.
Source
One file · copy & paste"use client";
import { type ReactNode, useState } from "react";
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Card, CardBody, CardHeader } from "oks-ui/card";
import { Chart } from "oks-ui/chart";
import { Chip } from "oks-ui/chip";
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "oks-ui/dropdown";
import { FormFieldSet } from "oks-ui/form-field-set";
import type { NavItemData } from "oks-ui/nav";
import { PageTitle } from "oks-ui/page-title";
import { Progress } from "oks-ui/progress";
import { Table } from "oks-ui/table";
import "oks-ui/avatar.css";
import "oks-ui/button.css";
import "oks-ui/card.css";
import "oks-ui/chart.css";
import "oks-ui/chip.css";
import "oks-ui/dropdown.css";
import "oks-ui/form-field-set.css";
import "oks-ui/page-title.css";
import "oks-ui/progress.css";
import "oks-ui/table.css";
import { PatternAppShell } from "../PatternAppShell";
import {
ArrowDownLeftIcon,
ArrowDownRightIcon,
ArrowsRightLeftIcon,
ArrowUpRightIcon,
BuildingIcon,
CalendarIcon,
ChevronDownIcon,
ClockIcon,
CreditCardIcon,
DownloadIcon,
GlobeIcon,
GridIcon,
HelpCircleIcon,
MailIcon,
MessageCircleIcon,
MoreIcon,
PieChartIcon,
PlusIcon,
ReceiptIcon,
SearchIcon,
SendIcon,
} from "../../showcase/icons";
const NAV_ITEMS: NavItemData[] = [
{
key: "main",
label: "Main menu",
isSection: true,
children: [
{ key: "dashboard", label: "Dashboard", icon: <GridIcon size={18} /> },
{ key: "analytics", label: "Analytics", icon: <PieChartIcon size={18} />, badge: 20 },
{ key: "transactions", label: "Transactions", icon: <ArrowsRightLeftIcon size={18} /> },
{ key: "invoices", label: "Invoices", icon: <ReceiptIcon size={18} /> },
],
},
{
key: "features",
label: "Features",
isSection: true,
children: [
{ key: "recurring", label: "Recurring", icon: <ClockIcon size={18} />, badge: 16 },
{ key: "subscriptions", label: "Subscriptions", icon: <CreditCardIcon size={18} /> },
{ key: "feedback", label: "Feedback", icon: <MessageCircleIcon size={18} /> },
],
},
// Settings / Help desk / Log out live in the account menu at the bottom
// of the sidebar (opened from the profile footer), not as permanent nav
// rows — they're account actions, not app destinations.
];
const wallets = [
{ flag: "🇺🇸", code: "USD", balance: "$22,678.00", status: "Active" as const },
{ flag: "🇪🇺", code: "EUR", balance: "€14,320.00", status: "Active" as const },
{ flag: "🇬🇧", code: "GBP", balance: "£9,540.00", status: "Active" as const },
{ flag: "🇯🇵", code: "JPY", balance: "¥1,240,000", status: "Inactive" as const },
];
const CURRENCIES = wallets.map((w) => ({ flag: w.flag, code: w.code }));
const earnings = [
{ month: "Jan", value: 21400 }, { month: "Feb", value: 24800 }, { month: "Mar", value: 19600 },
{ month: "Apr", value: 27200 }, { month: "May", value: 16800 }, { month: "Jun", value: 25400 },
{ month: "Jul", value: 18200 }, { month: "Aug", value: 34900 }, { month: "Sep", value: 23100 },
{ month: "Oct", value: 15600 }, { month: "Nov", value: 29800 }, { month: "Dec", value: 20200 },
];
const savingsGoals = [
{ id: 1, icon: <PieChartIcon size={18} />, label: "Investment goal", saved: "$15,600", target: "$25,000", pct: 62, color: "primary" as const },
{ id: 2, icon: <ReceiptIcon size={18} />, label: "Emergency fund", saved: "$8,400", target: "$12,000", pct: 70, color: "danger" as const },
{ id: 3, icon: <GlobeIcon size={18} />, label: "Vacation fund", saved: "$3,200", target: "$6,000", pct: 53, color: "info" as const },
{ id: 4, icon: <BuildingIcon size={18} />, label: "Home renovation", saved: "$11,400", target: "$20,000", pct: 57, color: "success" as const },
];
type TxnRow = {
id: number;
activity: string;
icon: ReactNode;
color: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "default";
date: string;
amount: string;
status: "Success" | "Pending" | "Failed";
} & Record<string, unknown>;
const transactions: TxnRow[] = [
{ id: 1, activity: "Software license", icon: <CreditCardIcon size={16} />, color: "secondary" as const, date: "Wed, 10 Jun-2026", amount: "$89.00", status: "Success" },
{ id: 2, activity: "Mobile app purchase", icon: <GridIcon size={16} />, color: "info" as const, date: "Tue, 09 Jun-2026", amount: "$46.50", status: "Success" },
{ id: 3, activity: "Grocery purchase", icon: <ReceiptIcon size={16} />, color: "warning" as const, date: "Sun, 07 Jun-2026", amount: "$132.00", status: "Success" },
{ id: 4, activity: "Freelance payment", icon: <ArrowDownLeftIcon size={16} />, color: "success" as const, date: "Fri, 05 Jun-2026", amount: "$2,150.00", status: "Success" },
{ id: 5, activity: "Subscription renewal", icon: <ClockIcon size={16} />, color: "default" as const, date: "Wed, 03 Jun-2026", amount: "$14.99", status: "Pending" },
{ id: 6, activity: "Wire transfer", icon: <ArrowsRightLeftIcon size={16} />, color: "danger" as const, date: "Mon, 01 Jun-2026", amount: "$500.00", status: "Failed" },
];
const statusTone: Record<TxnRow["status"], "success" | "warning" | "danger"> = {
Success: "success",
Pending: "warning",
Failed: "danger",
};
function DeltaChip({ delta, up }: { delta: string; up: boolean }) {
return (
<Chip
size="sm"
variant="soft"
color={up ? "success" : "danger"}
endContent={up ? <ArrowUpRightIcon size={12} /> : <ArrowDownRightIcon size={12} />}
>
{delta}
</Chip>
);
}
function MoreButton({ label, size = "sm" }: { label: string; size?: "xs-sm" | "sm" }) {
return (
<Dropdown placement="bottom-end">
<DropdownTrigger>
<Button isIconOnly aria-label={label} variant="ghost" radius="full" size={size}>
<MoreIcon size={size === "xs-sm" ? 14 : 16} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label={label}>
<DropdownItem key="view">View details</DropdownItem>
<DropdownItem key="export">Export</DropdownItem>
<DropdownItem key="dismiss" classNames={{ title: "text-[color:var(--oks-color-danger-700)]" }}>
Dismiss
</DropdownItem>
</DropdownMenu>
</Dropdown>
);
}
function StatCard({
icon,
title,
value,
delta,
up,
}: {
icon: ReactNode;
title: string;
value: string;
delta: string;
up: boolean;
}) {
return (
<Card shadow="sm" className="h-full">
<CardBody className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2.5">
<Avatar icon={icon as never} color="primary" radius="md" size="sm" />
<PageTitle
as="h3"
title={title}
classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
/>
</div>
<MoreButton label={`${title} options`} />
</div>
<div>
<p className="text-[1.7rem] font-bold tracking-tight text-ink">{value}</p>
<div className="mt-2 flex items-center gap-2 text-[0.8rem] text-ink-muted">
<DeltaChip delta={delta} up={up} />
from last month
</div>
</div>
</CardBody>
</Card>
);
}
export function AnalyticsDashboard() {
const [selectedKey, setSelectedKey] = useState("dashboard");
const [currency, setCurrency] = useState("USD");
const activeCurrency = CURRENCIES.find((c) => c.code === currency) ?? CURRENCIES[0];
return (
<PatternAppShell
navItems={NAV_ITEMS}
selectedNavKey={selectedKey}
onSelectedNavKeyChange={setSelectedKey}
headerLeft={
<FormFieldSet
type="search"
name="search"
label="Search"
placeholder="Search"
startIcon={<SearchIcon size={16} />}
endIcon={
<span className="flex items-center gap-1" aria-hidden="true">
<kbd className="rounded-[var(--r-sm)] bg-paper px-1.5 py-0.5 text-[0.68rem] font-medium text-ink-faint">⌘</kbd>
<kbd className="rounded-[var(--r-sm)] bg-paper px-1.5 py-0.5 text-[0.68rem] font-medium text-ink-faint">K</kbd>
</span>
}
classNames={{
base: "w-full max-w-xs",
label: "sr-only",
control: "border-transparent bg-paper-sunken",
}}
/>
}
headerExtra={
<>
<Button isIconOnly aria-label="Help" variant="ghost" radius="full" size="sm">
<HelpCircleIcon size={18} />
</Button>
<Button isIconOnly aria-label="Messages" variant="ghost" radius="full" size="sm">
<MailIcon size={18} />
</Button>
</>
}
>
{/* welcome banner. shrink-0 on every direct child of this
scroll body: a flex child defaults to flex-shrink:1 and
gets silently compressed to fit its allocated space instead
of overflowing, so without this the parent's overflow-y-auto
never sees anything to scroll and content gets clipped by
the outer overflow-hidden shell instead. */}
<div className="flex shrink-0 flex-wrap items-start justify-between gap-4">
<PageTitle
as="h2"
title="Welcome back, Omkar"
subtitle="Monitor and control what happens with your money today."
classNames={{
base: "flex-col items-start",
title: "text-2xl font-bold tracking-tight text-ink",
subtitle: "text-[0.9rem] text-ink-muted",
}}
/>
<div className="flex items-center gap-2">
<Button variant="bordered" radius="full" size="sm" startContent={<CalendarIcon size={14} />}>
Fri, 12 June 2026
</Button>
<Button color="primary" radius="full" size="sm" startContent={<DownloadIcon size={14} />}>
Export
</Button>
</div>
</div>
{/* One grid, not two stacked ones: Account balance + My wallet is
a SINGLE card spanning both rows (@3xl:row-span-2), sitting
beside Total expenses / Total savings (row 1) and Overview
(row 2, spanning the same two narrower columns). Splitting
"Account balance" and "My wallet" into separate cards was the
actual bug — they're one continuous section in the
reference, not two. */}
<div className="grid shrink-0 items-start gap-5 @3xl:grid-cols-[1.25fr_1fr_1fr] @3xl:grid-rows-[auto_auto]">
<Card shadow="sm" className="@3xl:row-span-2">
<CardBody className="flex flex-col gap-5">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2.5">
<Avatar icon={<CreditCardIcon size={18} />} color="primary" radius="md" size="sm" />
<PageTitle
as="h3"
title="Account balance"
classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
/>
</div>
<Dropdown placement="bottom-end">
<DropdownTrigger>
<Button
variant="bordered"
radius="full"
size="xs"
startContent={<span aria-hidden="true">{activeCurrency.flag}</span>}
endContent={<ChevronDownIcon size={14} />}
>
{activeCurrency.code}
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Choose currency"
selectionMode="single"
selectedKeys={[currency]}
onAction={(key) => setCurrency(String(key))}
>
{CURRENCIES.map((c) => (
<DropdownItem key={c.code} startContent={<span aria-hidden="true">{c.flag}</span>}>
{c.code}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
<div>
<p className="text-[2rem] font-bold tracking-tight text-ink">$48,920.15</p>
<div className="mt-2 flex items-center gap-2 text-[0.8rem] text-ink-muted">
<DeltaChip delta="+4.8%" up />
from last month
</div>
</div>
{/* flex-1 on both: a real 50/50 split instead of each
button hugging its own label width. */}
<div className="flex gap-2">
<Button color="primary" radius="full" size="xs" className="flex-1" startContent={<SendIcon size={12} />}>
Send money
</Button>
<Button variant="bordered" radius="full" size="xs" className="flex-1" startContent={<ArrowDownLeftIcon size={12} />}>
Request money
</Button>
</div>
<div className="mt-1 flex flex-col gap-3">
<div className="flex items-center justify-between">
<PageTitle
as="h3"
title="My wallet"
classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
/>
<Button variant="bordered" radius="full" size="xs" startContent={<PlusIcon size={12} />}>
Add new
</Button>
</div>
<div className="grid grid-cols-2 gap-3">
{wallets.map((w) => (
<div key={w.code} className="relative rounded-[var(--r-md)] bg-paper-sunken p-3.5">
<div className="absolute right-1.5 top-1.5">
<MoreButton label={`${w.code} wallet options`} size="xs-sm" />
</div>
<div className="flex items-center gap-2 text-[0.85rem] font-medium text-ink">
<span aria-hidden="true">{w.flag}</span>
{w.code}
</div>
<p className="mt-2 text-[1.05rem] font-semibold text-ink">{w.balance}</p>
<p
className="mt-2 text-[0.8rem] font-medium"
style={{ color: `var(--oks-color-${w.status === "Active" ? "success" : "warning"}-700)` }}
>
{w.status}
</p>
</div>
))}
</div>
</div>
</CardBody>
</Card>
<StatCard icon={<ArrowsRightLeftIcon size={18} />} title="Total expenses" value="$12,340.60" delta="-1.6%" up={false} />
<StatCard icon={<PieChartIcon size={18} />} title="Total savings" value="$22,150.40" delta="+6.2%" up />
<Card shadow="sm" className="@3xl:col-span-2">
<CardHeader className="flex flex-wrap items-center justify-between gap-2">
<PageTitle
as="h3"
title="Overview"
classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
/>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1.5 text-[0.78rem] text-ink-muted">
<span
aria-hidden="true"
className="h-2 w-2 rounded-full"
style={{ background: "var(--oks-color-primary-600)" }}
/>
Earnings
</span>
<Button variant="ghost" size="sm" endContent={<ChevronDownIcon size={14} />}>
This year
</Button>
<MoreButton label="Overview options" />
</div>
</CardHeader>
<CardBody>
<Chart type="column" data={earnings} x="month" series="value" legend={false} height={220} />
</CardBody>
</Card>
</div>
{/* My savings plan / Recent transactions — same 3-column template
again, so all three rows share one consistent set of column
edges down the page. */}
<div className="grid shrink-0 gap-5 @3xl:grid-cols-[1.25fr_1fr_1fr]">
<Card shadow="sm">
<CardHeader className="flex items-center justify-between">
<PageTitle
as="h3"
title="My savings plan"
classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
/>
<MoreButton label="Savings plan options" />
</CardHeader>
<CardBody className="flex flex-col gap-6">
{savingsGoals.map((g) => (
<div key={g.id} className="flex flex-col gap-2.5">
<div className="flex items-center gap-2.5">
<Avatar icon={g.icon} color={g.color} radius="md" size="sm" />
<p className="text-[0.85rem] font-medium text-ink">{g.label}</p>
</div>
<div className="flex items-center justify-between text-[0.85rem]">
<span className="text-ink-muted">
{g.saved} / {g.target}
</span>
<span className="font-semibold text-ink">{g.pct}%</span>
</div>
{/* aria-hidden: the label + "$saved / $target" + "N%" above
already say everything this bar would announce again. */}
<Progress value={g.pct} color={g.color} size="sm" aria-hidden="true" />
</div>
))}
</CardBody>
</Card>
<Card shadow="sm" className="@3xl:col-span-2">
<CardHeader className="flex items-center justify-between">
<PageTitle
as="h3"
title="Recent transactions"
classNames={{ base: "flex-col items-start", title: "text-[0.95rem] font-semibold text-ink" }}
/>
<Button variant="bordered" size="sm">Filter</Button>
</CardHeader>
<CardBody>
<Table
aria-label="Recent transactions"
classNames={{ cell: "whitespace-nowrap" }}
columns={[
{
key: "activity",
header: "Activity",
render: (r: TxnRow) => (
<div className="flex items-center gap-2.5">
<Avatar icon={r.icon as never} color={r.color} radius="md" size="sm" />
<span className="font-medium text-ink">{r.activity}</span>
</div>
),
},
{ key: "date", header: "Date" },
{ key: "amount", header: "Amount", align: "end" },
{
key: "status",
header: "Status",
render: (r: TxnRow) => (
<Chip size="sm" variant="soft" color={statusTone[r.status]}>
{r.status}
</Chip>
),
},
{
key: "actions",
header: "",
ariaLabel: "Actions",
align: "end",
render: (r: TxnRow) => <MoreButton label={`${r.activity} options`} size="xs-sm" />,
},
]}
rows={transactions}
getRowKey={(r) => r.id}
/>
</CardBody>
</Card>
</div>
</PatternAppShell>
);
}