docs / core / card
Card
the base surface, composed not configured. header, body and footer are regions you fill, so a card is whatever you assemble in them. one surface word sets its weight, from elevated to a bare outline, and StatCard, Panel and Dialog all speak it.
intensity · recedesrsc · server
provenance
shaped from personal-website · sisu-pluslayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
holds Button, Badge, StatCard content, forms, anything groupedGlowCard swaps in for the single card that earns the pointer glownever a card inside a card. nest with Panel or plain spacinginteractive without an actual click target is a liea11y
CardTitle is a real<h3> · the glow layer is aria-hiddenjest-axedependencies
Badge from the same packagelive demo · try it out
customize
surfacehow the card sits on the page
highlightaccent wash, edge, or full ring
interactivelifts on hover for clickable cards
row (CardHeader)icon + title + actions inline
usage
import { Card, CardActions, CardBadge, CardBody, CardEyebrow, CardFooter, CardHeader, CardIcon, CardTitle } from "usva/primitives/card";
<Card interactive>
<CardHeader row>
<CardIcon><BoltIcon /></CardIcon>
<div className="flex flex-col gap-1">
<CardEyebrow>Deployment</CardEyebrow>
<CardTitle>Production build</CardTitle>
</div>
<CardActions>
<CardBadge tone="success">live</CardBadge>
</CardActions>
</CardHeader>
<CardBody>Shipped 4 minutes ago.</CardBody>
<CardFooter>
<Button size="sm" variant="outline">View logs</Button>
</CardFooter>
</Card>highlights
highlight = "wash"
pick a highlight
a radial accent wash bleeds atmosphere across the surface.
surfaceshared with StatCard · Panel · Dialog
surface = "elevated"
pick a surface
the default. lit-from-above fill, a rim light and a floating shadow.
props
| prop | type | default | notes |
|---|---|---|---|
| surface | "elevated" | "flat" | "glass" | "outline" | "elevated" | how the card sits on the page. the same vocabulary drives StatCard, Panel and Dialog. glass is a rare, purposeful choice, never a default. |
| highlight | "none" | "wash" | "edge" | "ring" | "none" | accent treatment: a radial wash, a top edge hairline, or a full glow ring. ring replaces the elevation shadow. |
| interactive | boolean | false | lifts on hover. only for cards that are actually clickable. |
| row (CardHeader) | boolean | false | lays the header out as icon + title + actions instead of stacked. |
get it
npx shadcn add https://usva.build/r/card.jsonsource · components/ui/card.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
import { Badge, type BadgeProps } from "./badge";
type Div = React.HTMLAttributes<HTMLDivElement>;
const make = (base: string, name: string) =>
Object.assign(
React.forwardRef<HTMLDivElement, Div>(({ className, ...p }, ref) => (
<div ref={ref} className={cn(base, className)} {...p} />
)),
{ displayName: name },
);
export type CardHighlight = "none" | "wash" | "edge" | "ring";
/**
* The shared surface vocabulary. One word picks how any card-like surface
* (Card, StatCard, Panel, Dialog) sits on the page, and the same choice reads
* consistently across all of them.
* elevated: the default. Lit-from-above surface fill, rim light, shadow.
* flat: quiet surface fill, no lift. The sisu/dashboard workhorse.
* glass: translucent, blurs what sits behind it. A rare, purposeful
* choice, never a default.
* outline: transparent, carried by its border alone.
*/
export type CardSurface = "elevated" | "flat" | "glass" | "outline";
/** Background + rim treatment per surface. No shadow; consumers pick the level. */
export const SURFACE_SKIN: Record<CardSurface, string> = {
elevated: "rim-light bg-surface",
flat: "bg-surface",
glass: "rim-light bg-surface/65 backdrop-blur-md",
outline: "bg-transparent",
};
/** Whether a surface reads as raised (gets its host's elevation shadow). */
export const SURFACE_ELEVATED: Record<CardSurface, boolean> = {
elevated: true,
flat: false,
glass: true,
outline: false,
};
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
interactive?: boolean;
highlight?: CardHighlight;
surface?: CardSurface;
/** @deprecated use `highlight="wash"` */
wash?: boolean;
}
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
(
{
className,
interactive,
highlight = "none",
surface = "elevated",
wash,
...p
},
ref,
) => {
const resolved: CardHighlight =
highlight === "none" && wash ? "wash" : highlight;
return (
<div
ref={ref}
data-interactive={interactive ? "" : undefined}
data-highlight={resolved !== "none" ? resolved : undefined}
data-surface={surface}
className={cn(
"relative isolate rounded-2xl border border-border text-ink",
SURFACE_SKIN[surface],
resolved === "ring"
? "glow-ring"
: SURFACE_ELEVATED[surface] && "shadow-floating",
resolved === "wash" && "wash-accent",
resolved === "edge" &&
"after:absolute after:inset-x-5 after:top-0 after:h-px after:hairline-accent",
interactive &&
"transition-control duration-base ease-soft hover:-translate-y-0.5 motion-reduce:transform-none motion-reduce:transition-none",
interactive && resolved !== "ring" && "hover:shadow-overlay",
className,
)}
{...p}
/>
);
},
);
Card.displayName = "Card";
export interface CardHeaderProps extends Div {
row?: boolean;
}
export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
({ className, row, ...p }, ref) => (
<div
ref={ref}
className={cn(
"relative p-5 after:absolute after:inset-x-5 after:bottom-0 after:h-px after:hairline-accent",
row ? "flex flex-row items-start gap-4" : "flex flex-col gap-1.5",
className,
)}
{...p}
/>
),
);
CardHeader.displayName = "CardHeader";
export const CardBody = make("p-5", "CardBody");
export const CardFooter = make(
"flex items-center gap-2 p-5 border-t border-border",
"CardFooter",
);
export const CardEyebrow = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...p }, ref) => (
<p
ref={ref}
className={cn(
"font-mono text-[0.65rem] uppercase leading-none tracking-[0.16em] text-muted",
className,
)}
{...p}
/>
));
CardEyebrow.displayName = "CardEyebrow";
export interface CardTitleProps
extends React.HTMLAttributes<HTMLHeadingElement> {
/** Heading level. Pick it to fit the page outline. */
as?: "h2" | "h3" | "h4" | "h5" | "h6";
}
export const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
({ className, as: Heading = "h3", ...p }, ref) => (
<Heading
ref={ref}
className={cn(
"text-balance text-sm font-semibold tracking-[-0.01em] text-ink",
className,
)}
{...p}
/>
),
);
CardTitle.displayName = "CardTitle";
export const CardIcon = make(
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-2 text-muted [&_svg]:h-4 [&_svg]:w-4",
"CardIcon",
);
export const CardActions = make(
"ml-auto flex items-center gap-1",
"CardActions",
);
export function CardBadge({ className, ...props }: BadgeProps) {
return <Badge className={cn("self-start", className)} {...props} />;
}
CardBadge.displayName = "CardBadge";
source · components/ui/glow-card.tsxexactly what this command copies
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Card, type CardProps } from "./card";
/**
* A Card whose border lights up on the edge facing the pointer. The arc turns
* to follow the cursor and brightens as you approach the edge, so a single card
* gets the same directional glow the BentoGrid shares across a whole grid.
*/
export const GlowCard = React.forwardRef<HTMLDivElement, CardProps>(
({ className, children, ...props }, ref) => {
const localRef = React.useRef<HTMLDivElement | null>(null);
const setRefs = React.useCallback(
(node: HTMLDivElement | null) => {
localRef.current = node;
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
},
[ref],
);
const onPointerMove = React.useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
const el = localRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
el.style.setProperty("--edge-x", `${e.clientX - r.left}px`);
el.style.setProperty("--edge-y", `${e.clientY - r.top}px`);
el.style.setProperty("--edge-o", "1");
},
[],
);
const onPointerLeave = React.useCallback(() => {
localRef.current?.style.setProperty("--edge-o", "0");
}, []);
return (
<Card
ref={setRefs}
className={cn("relative", className)}
onPointerMove={onPointerMove}
onPointerLeave={onPointerLeave}
{...props}
>
<span aria-hidden className="edge-glow" />
{children}
</Card>
);
},
);
GlowCard.displayName = "GlowCard";