docs / patterns / panel
Panel
the dashboard workhorse: it fills its grid cell, the header stays fixed, and the body scrolls its own overflow.
intensity · structuresrsc · server
provenance
shaped from sisu-pluslayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
a cell of DashboardGrid or a bento layout. it needs a sized boxbadge takes a Badge, actions take icon-only Buttonsnever nested in a Card or another Panel. it is the regionnot a page section on a marketing page. that is Card territorya11y
the loading spinner isrole="status" with an sr label · badge and actions keep their own semanticsjest-axedependencies
Spinner · Card from the same packagelive demo · try it out
overview
Deployments
live
- building
feat/docs
1ec3bbc · just now
- live
fix/hero-split
0df772e · 14m ago
- live
chore/tokens
c74cf7c · 1h ago
- live
feat/color-field
fa17ccd · 3h ago
- failed
fix/bento-grid
9d2d01e · 5h ago
- live
feat/segmented
8b21f04 · yesterday
- queued
chore/registry
a90c7d1 · yesterday
customize
eyebrowmono label above the title
titlethe panel heading
surfacehow it sits on the page
badgea status Badge in the header
loadingswaps the body for a Spinner
usage
import { Panel } from "usva/patterns/panel";
import { Badge } from "usva/primitives/badge";
<Panel
eyebrow="overview"
title="Deployments"
badge={<Badge tone="accent-alt" live>live</Badge>}
>
<DeploymentList />
</Panel>props
| prop | type | default | notes |
|---|---|---|---|
| title | ReactNode | — | the panel heading. |
| eyebrow | ReactNode | — | mono uppercase label above the title. |
| icon | ReactNode | — | leading icon, shown in a tile. |
| badge | ReactNode | — | right-aligned status slot. pass a Badge. |
| actions | ReactNode | — | right-aligned controls slot. icon-only Buttons fit. |
| loading | boolean | false | swaps the body for a centered Spinner. the header stays. |
| loadingSlot | ReactNode | — | replaces the default spinner while loading. a Skeleton stack goes here. |
| surface | CardSurface | "elevated" | how the panel sits on the page. shares Card's surface scale. |
get it
npx shadcn add https://usva.build/r/panel.jsonsource · components/ui/panel.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
import {
type CardSurface,
SURFACE_ELEVATED,
SURFACE_SKIN,
} from "./card";
import { Spinner } from "./spinner";
export interface PanelProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
icon?: React.ReactNode;
eyebrow?: React.ReactNode;
title: React.ReactNode;
badge?: React.ReactNode;
actions?: React.ReactNode;
loading?: boolean;
/** Replaces the default centered spinner while `loading`. */
loadingSlot?: React.ReactNode;
/** How the panel sits on the page. Defaults to elevated. */
surface?: CardSurface;
}
/**
* The dashboard panel. A full-height card with a mono eyebrow header, an icon
* tile, badge/actions slots, and a loading state. Built to live in a grid cell:
* it fills its box and scrolls its body. usva's "usability pole" workhorse.
*/
export const Panel = React.forwardRef<HTMLDivElement, PanelProps>(
(
{
className,
icon,
eyebrow,
title,
badge,
actions,
loading = false,
loadingSlot,
surface = "elevated",
children,
...props
},
ref,
) => (
<div
ref={ref}
data-surface={surface}
className={cn(
"relative flex h-full min-h-0 w-full flex-col overflow-hidden rounded-2xl border border-border text-ink",
SURFACE_SKIN[surface],
SURFACE_ELEVATED[surface] && "shadow-floating",
className,
)}
{...props}
>
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
<div className="flex min-w-0 flex-1 items-center gap-2.5">
{icon != null && (
<span className="grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-surface-2 text-muted [&_svg]:h-4 [&_svg]:w-4">
{icon}
</span>
)}
<div className="min-w-0">
{eyebrow != null && (
<p className="font-mono text-[0.625rem] font-semibold uppercase leading-none tracking-[0.12em] text-muted">
{eyebrow}
</p>
)}
<div className="mt-1 truncate text-sm font-semibold tracking-[-0.01em] text-ink">
{title}
</div>
</div>
</div>
{(badge != null || actions != null) && (
<div className="flex shrink-0 items-center gap-2">
{badge}
{actions}
</div>
)}
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
{loading
? (loadingSlot ?? (
<div className="grid h-full min-h-24 place-items-center">
<Spinner />
</div>
))
: children}
</div>
</div>
),
);
Panel.displayName = "Panel";