An article
A run, a report, anything a reader acts on as one object.
Components
A container for one object: quiet by default, interactive only when the whole surface is genuinely clickable.
Choose elevation by how far the surface is from the canvas, not by how important it feels.
A hairline and a small shadow on the canvas. The workhorse.
Brighter surface and deeper shadow. For stat cards and focused content.
Transparent shell. Use inside an existing surface or over media.
Grouping only. No chrome until it is hovered.
<Card variant="default">A hairline and a small shadow on the canvas.</Card>
<Card variant="raised">Brighter surface, deeper shadow — stat cards.</Card>
<Card variant="outline">Transparent shell, for use inside another surface.</Card>
<Card variant="ghost">Grouping only. No chrome until it is hovered.</Card>CardHeader, CardContent and CardFooter give every card the same internal rhythm.
raw → analytics → validation → summary
Seven analytics domains run after ingestion, then a read-only validation pass records a Lark summary.
Panels have no elevation. They structure content inside a surface without adding another visual layer.
<Card variant="raised">
<CardHeader
icon={<RiStackLine />}
actions={<Badge size="sm" tone="info">nightly</Badge>}
>
<CardTitle>Research pipeline</CardTitle>
<CardDescription>raw → analytics → validation → summary</CardDescription>
</CardHeader>
<CardContent>
Seven analytics domains run after ingestion, then a read-only validation pass.
</CardContent>
<CardFooter>
<Button size="sm" variant="secondary">Open run</Button>
<Button size="sm" variant="ghost">Copy id</Button>
</CardFooter>
</Card>Interaction adds a brighter border, one elevation step and half a pixel of travel. Selection uses the accent border plus a restrained glow.
No hover response. It is not clickable, so it does not pretend to be.
Hover me. Border, surface and shadow each move exactly one step.
Accent border, soft accent tint, subtle glow. Click to toggle.
The loudest static mode: a brighter border and a subtle accent glow at rest, without pretending to be clickable.
<Card variant="outline">Static: no hover response, because it is not clickable.</Card>
<Card as="a" href="/runs/128" interaction="interactive">
Border, surface and shadow each move exactly one step.
</Card>
<Card interaction="selectable" selected={selected} onClick={toggle}>
Selected uses the accent border, never a filled accent surface.
</Card>
<Card interaction="featured">
The loudest static mode: a brighter border and a subtle accent glow at rest.
</Card>`padding` is the shell's own rhythm — `none` hands it to the parts, which carry their own. `as` picks the element, so a card that holds an article or a page section appears in the document outline as one.
The default rhythm for a card that carries its own content.
A run, a report, anything a reader acts on as one object.
A region with a heading of its own; tight padding keeps it quiet.
<Card padding="md">The shell sets the inner rhythm.</Card>
<Card as="article" variant="raised" padding="lg">
An article inside a list of runs.
</Card>
<Card as="section" variant="outline" padding="sm">
A page section. It says so in the outline.
</Card>import { Card, CardHeader, CardContent, CardFooter } from "@luc/ui";<Card>
<CardHeader>
<CardTitle>Research pipeline</CardTitle>
<CardDescription>raw → analytics → validation → summary</CardDescription>
</CardHeader>
<CardContent>Seven analytics domains run after ingestion.</CardContent>
<CardFooter>
<Button size="sm" variant="secondary">Open run</Button>
</CardFooter>
</Card>`Panel`, `PanelHeader` and `PanelBody` come from the same package and are what a region inside a card uses: `import { Panel, PanelHeader, PanelBody } from "@luc/ui";`.
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
Card | 9 documented props | ({ className, variant, interactive, interaction, selected, padding, as, ...props }: CardProps) => import("@types/react").JSX.Element |
CardContent | export | ({ className, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element |
CardDescription | export | ({ className, ...props }: React.ComponentPropsWithRef<"p">) => import("@types/react").JSX.Element |
CardFooter | export | ({ className, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element |
CardHeader | 2 documented props | ({ className, actions, icon, children, ...props }: CardHeaderProps) => import("@types/react").JSX.Element |
CardTitle | export | ({ className, children, ...props }: React.ComponentPropsWithRef<"h3">) => import("@types/react").JSX.Element |
cardVariants | export | (props?: ({ variant?: "outline" | "ghost" | "default" | "raised" | null | undefined; interactive?: boolean | null | undefined; interaction?: "interactive" | "static" | "selectable" | "featured" | nul… |
Type: CardProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
as? | "a" | "button" | "div" | "li" | "article" | "section" | undefined | — | |
href? | string | undefined | — | Anchor target. Only meaningful when `as="a"`. |
interaction? | "interactive" | "static" | "selectable" | "featured" | null | undefined | — | |
interactive? | boolean | null | undefined | — | |
padding? | "none" | "sm" | "md" | "lg" | null | undefined | — | |
rel? | string | undefined | — | |
selected? | boolean | null | undefined | — | |
target? | string | undefined | — | |
variant? | "outline" | "ghost" | "default" | "raised" | null | undefined | — |
Type: CardHeaderProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
actions? | ReactNode | — | Right-aligned slot for a menu, badge or icon button. |
icon? | ReactNode | — | Leading slot for an icon or avatar. |
Do
Don't
Interaction is opt-in and the whole surface becomes the control: `interaction="interactive"` or `interaction="selectable"` renders the element `as` names — a real `<button>` or `<a href>` — so Enter, Space and focus order come from the browser. That is also why an interactive card must not contain another interactive card, and why a selected card carries `data-selected` as well as its accent border: the state is in the DOM, not only in the pixels.
Related