FoundationsCard
A compound surface — header, content, footer on the card token.
Sound layer
wave aone capturing listener, six synthesized gestures.
anything with an interactive role taps on click. components that own a paired sound mark themselves data-silent and the layer steps aside.
The thinking
A card exists to say "this region is one thing, and it sits a step above the page." I build it as compound parts —
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter — instead of a title/footer prop bag, so the markup reads in the order it renders and you compose only the rows you need. The one load-bearing decision is depth without a drop shadow: the lift comes entirely from where --card sits on the surface ladder, not from a blur. That's why the title takes font-head at font-medium while the description drops to text-muted-foreground — one surface, but a clear hierarchy inside it. The small things carry it: text-balance on the title and text-pretty on the description so neither strands a widow, and a single p-4 rhythm with pt-0 on the lower rows so the header's spacing flows straight into the content rather than stacking two paddings.Interface design
The card is where the depth ladder shows itself. In light mode
--card is pure white, and white stays reserved for interactive chrome — so a card reads as a surface you can act on, lifted off the warm --background ground. Dark mode has no darker-means-deeper trick: surfaces lift lighter off the L .19 ground — secondary at .23, card at .25, muted at .28 — and the card still sits a step above the page. The edge is border-faint — four percent ink in light, eight on the dark ground, where hairlines need more presence — because at white-on-ground contrast the border only needs to be suggested.Interaction
Nothing here moves, and that's the decision. There is no hover state, no press scale, not one transition in the source — in my practice, motion on a surface is a promise of a click, and a plain card can't keep that promise. The feedback lives where the intent does: the controls placed inside the card carry their own press and hover states while the surface under them holds still. A card that stirs when you point at it is inferring intent that isn't there.
Sound design
The card never speaks, and it doesn't even have to opt out. The global SoundLayer plays its tap through one capturing click listener that matches only elements carrying an interactive role —
a[href], button, the aria widget roles — and a plain div is not on that list, so a click on the card's padding meets nothing. The buttons in the footer still tap, because the listener finds them by their own role, not by the surface they sit on. Silence by having no role beats silence by data-silent — there's nothing to mark and no way to get it wrong.Empathy
The accessibility here is done by the tags, not by aria.
CardTitle renders a real h3 and CardDescription a real p, so a screen reader hears a heading and its text instead of a stack of styled divs — no roles are wired because none are needed on static content. There is no disabled state, no focus ring, no reduced-motion branch, and honestly there's nothing to disable, focus, or animate; those cases belong to the controls placed inside. Every part spreads its props and takes a ref, so when a card does need a label or a region role the consumer wires it without forking the file. And nothing clamps: no fixed heights, no truncation, so long content grows the card instead of clipping against it.import * as React from "react";
import { cn } from "@/lib/utils";
type DivProps = React.HTMLAttributes<HTMLDivElement> & {
ref?: React.Ref<HTMLDivElement>;
};
// in light mode --card is white, and white stays reserved for interactive
// chrome — the hairline is border-faint because at that contrast the edge
// only needs to be suggested
const Card = ({ ref, className, ...props }: DivProps) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-border-faint bg-card text-card-foreground",
className
)}
{...props}
/>
);
Card.displayName = "Card";
const CardHeader = ({ ref, className, ...props }: DivProps) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1 p-4", className)}
{...props}
/>
);
CardHeader.displayName = "CardHeader";
const CardTitle = ({
ref,
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement> & {
ref?: React.Ref<HTMLHeadingElement>;
}) => (
<h3
ref={ref}
className={cn("font-head text-base font-medium text-balance", className)}
{...props}
/>
);
CardTitle.displayName = "CardTitle";
const CardDescription = ({
ref,
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement> & {
ref?: React.Ref<HTMLParagraphElement>;
}) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground text-pretty", className)}
{...props}
/>
);
CardDescription.displayName = "CardDescription";
const CardContent = ({ ref, className, ...props }: DivProps) => (
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
);
CardContent.displayName = "CardContent";
const CardFooter = ({ ref, className, ...props }: DivProps) => (
<div
ref={ref}
className={cn("flex items-center p-4 pt-0", className)}
{...props}
/>
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };