AZMARA PLATFORM
Examples

Dashboard shell

A navigation list, a top bar with a user menu, stat cards with a loading state, and tabs, laid out with the grid.

View as Markdown for coding assistants, or see llms.txt.

The frame most admin screens share. It is the part to copy first, because every page of an app goes inside it.

Preview theme
Preview density

Overview

Revenue

$48,200

+12%
Customers

1,284

+38
Open invoices

17

+4
Churn

1.9%

-0.3%

Figures are up to date

x
x
"use client";

import { Avatar } from "@azmr/ui/avatar";
import { Breadcrumbs } from "@azmr/ui/breadcrumb";
import { Button } from "@azmr/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@azmr/ui/disclosure";
import {
  Badge,
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
  Skeleton,
} from "@azmr/ui/display";
import { Icon } from "@azmr/ui/icons";
import { Grid } from "@azmr/ui/layout";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuTrigger,
} from "@azmr/ui/menu";
import { Progress } from "@azmr/ui/progress";
import { CreditCard, LayoutDashboard, RefreshCw, Settings, Users } from "lucide-react";
import { useEffect, useState } from "react";

const NAV = [
  { id: "overview", label: "Overview", icon: LayoutDashboard },
  { id: "customers", label: "Customers", icon: Users },
  { id: "billing", label: "Billing", icon: CreditCard },
  { id: "settings", label: "Settings", icon: Settings },
];

const STATS = [
  { label: "Revenue", value: "$48,200", change: "+12%", tone: "success" as const },
  { label: "Customers", value: "1,284", change: "+38", tone: "success" as const },
  { label: "Open invoices", value: "17", change: "+4", tone: "warning" as const },
  { label: "Churn", value: "1.9%", change: "-0.3%", tone: "success" as const },
];

const ACTIVITY = [
  "Hemi Te Rangi upgraded to Enterprise",
  "Invoice INV-204 was paid by Aroha Ngata",
  "Tane Smith's card failed",
  "Mere Walker started a trial",
];

// A dashboard shell is a navigation list, a top bar and a content area. The navigation marks the
// current page with aria-current, and the content area is a labelled region, so a screen reader
// user can jump to either. Real apps would use their router's links where these use #.
export default function DashboardShell() {
  const [page, setPage] = useState("overview");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!loading) return;
    const timer = setTimeout(() => setLoading(false), 1200);
    return () => clearTimeout(timer);
  }, [loading]);

  const current = NAV.find((item) => item.id === page) ?? NAV[0];

  return (
    <div className="overflow-hidden rounded-lg border border-border bg-background">
      <div className="grid md:grid-cols-[13rem_1fr]">
        <nav
          aria-label="Dashboard"
          className="border-b border-border bg-muted/40 p-3 md:border-r md:border-b-0"
        >
          <ul className="m-0 flex list-none flex-row gap-1 p-0 md:flex-col">
            {NAV.map((item) => (
              <li key={item.id}>
                <a
                  href={`#${item.id}`}
                  aria-current={page === item.id ? "page" : undefined}
                  onClick={(event) => {
                    event.preventDefault();
                    setPage(item.id);
                  }}
                  className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-foreground hover:bg-accent aria-[current=page]:bg-accent aria-[current=page]:font-medium"
                >
                  <Icon icon={item.icon} />
                  {item.label}
                </a>
              </li>
            ))}
          </ul>
        </nav>

        <section aria-label={current?.label} className="flex min-w-0 flex-col gap-5 p-5">
          <div className="flex flex-wrap items-center justify-between gap-3">
            <Breadcrumbs
              label="You are here"
              items={[{ label: "Acme", href: "#acme" }, { label: current?.label ?? "" }]}
            />
            <div className="flex items-center gap-2">
              <Button
                variant="outline"
                size="sm"
                startIcon={<Icon icon={RefreshCw} />}
                loading={loading}
                loadingText="Refreshing"
                onClick={() => setLoading(true)}
              >
                Refresh
              </Button>
              <DropdownMenu>
                <DropdownMenuTrigger
                  render={
                    <button type="button" aria-label="Account menu" className="rounded-full">
                      <Avatar name="Aroha Ngata" size="sm" />
                    </button>
                  }
                />
                <DropdownMenuContent>
                  {/* A label has to sit inside a group, which is also how it is announced. */}
                  <DropdownMenuGroup>
                    <DropdownMenuLabel>aroha@example.co.nz</DropdownMenuLabel>
                    <DropdownMenuItem>Profile</DropdownMenuItem>
                    <DropdownMenuItem>Sign out</DropdownMenuItem>
                  </DropdownMenuGroup>
                </DropdownMenuContent>
              </DropdownMenu>
            </div>
          </div>

          <h2 className="m-0 text-xl font-semibold">{current?.label}</h2>

          <Grid minItemWidth="9rem" gap={4} aria-busy={loading}>
            {STATS.map((stat) => (
              <Card key={stat.label}>
                <CardHeader>
                  <CardDescription>{stat.label}</CardDescription>
                  {loading ? (
                    <Skeleton className="h-7 w-24" />
                  ) : (
                    <CardTitle className="text-2xl">{stat.value}</CardTitle>
                  )}
                </CardHeader>
                <CardContent>
                  {!loading && <Badge variant={stat.tone}>{stat.change}</Badge>}
                </CardContent>
              </Card>
            ))}
          </Grid>
          <p className="sr-only" aria-live="polite">
            {loading ? "Refreshing figures" : "Figures are up to date"}
          </p>

          <Tabs defaultValue="usage">
            <TabsList aria-label="Details">
              <TabsTrigger value="usage">Usage</TabsTrigger>
              <TabsTrigger value="activity">Activity</TabsTrigger>
            </TabsList>
            <TabsContent value="usage" className="flex max-w-md flex-col gap-4 py-4">
              <Progress value={72} showValue aria-label="Storage used" />
              <Progress value={91} tone="warning" aria-label="Seats used" />
            </TabsContent>
            <TabsContent value="activity" className="py-4">
              <ul className="m-0 flex list-none flex-col gap-2 p-0 text-sm">
                {ACTIVITY.map((line) => (
                  <li key={line}>{line}</li>
                ))}
              </ul>
            </TabsContent>
          </Tabs>
        </section>
      </div>
    </div>
  );
}

What it shows

  • Landmarks. The navigation is a nav with a label and marks the current page with aria-current="page". The content is a labelled section, so a screen reader user can jump straight to it. The example does not use a main element, because this page already has one; in an app, make the content area your main.
  • A responsive layout. The navigation is a row on a narrow screen and a column from the md breakpoint, with the content filling the rest. See Layout for Grid, which also sizes the stat cards (minItemWidth).
  • A trail. Breadcrumbs say where the page sits. The last item is always the current page, so it is not a link.
  • A user menu on an avatar. The account button has its own label, because the Avatar inside it is a picture. The menu is the standard dropdown.
  • Loading without layout shift. Refresh swaps each figure for a Skeleton of about the same size and sets aria-busy on the grid, and a visually hidden live region says "Refreshing figures" and "Figures are up to date".
  • Tabs for related detail. Tabs with arrow-key navigation, and Progress bars with names.

Limits

The links use # addresses and a state variable for the current page. In a real app, use your router's link component, set aria-current from the route, and replace the figures with data.

On this page