Skip to content

Components

Cards and panels

A container for one object: quiet by default, interactive only when the whole surface is genuinely clickable.

Examples

Variants

Choose elevation by how far the surface is from the canvas, not by how important it feels.

Default

A hairline and a small shadow on the canvas. The workhorse.

Raised

Brighter surface and deeper shadow. For stat cards and focused content.

Outline

Transparent shell. Use inside an existing surface or over media.

Ghost

Grouping only. No chrome until it is hovered.

Composition

CardHeader, CardContent and CardFooter give every card the same internal rhythm.

Research pipeline

raw → analytics → validation → summary

nightly

Seven analytics domains run after ingestion, then a read-only validation pass records a Lark summary.

Panel header

Panels have no elevation. They structure content inside a surface without adding another visual layer.

denseno shadow

Interactive and selected

Interaction adds a brighter border, one elevation step and half a pixel of travel. Selection uses the accent border plus a restrained glow.

Static

No hover response. It is not clickable, so it does not pretend to be.

Interactive

Hover me. Border, surface and shadow each move exactly one step.

Selectable

active

Accent border, soft accent tint, subtle glow. Click to toggle.

Featured

The loudest static mode: a brighter border and a subtle accent glow at rest, without pretending to be clickable.

Padding, and the element it renders as

`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.

Medium padding

The default rhythm for a card that carries its own content.

An article

A run, a report, anything a reader acts on as one object.

A section

A region with a heading of its own; tight padding keeps it quiet.

Usage

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";`.

API

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.

Exported symbols
ExportKindSignature
Card9 documented props({ className, variant, interactive, interaction, selected, padding, as, ...props }: CardProps) => import("@types/react").JSX.Element
CardContentexport({ className, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element
CardDescriptionexport({ className, ...props }: React.ComponentPropsWithRef<"p">) => import("@types/react").JSX.Element
CardFooterexport({ className, ...props }: React.ComponentPropsWithRef<"div">) => import("@types/react").JSX.Element
CardHeader2 documented props({ className, actions, icon, children, ...props }: CardHeaderProps) => import("@types/react").JSX.Element
CardTitleexport({ className, children, ...props }: React.ComponentPropsWithRef<"h3">) => import("@types/react").JSX.Element
cardVariantsexport(props?: ({ variant?: "outline" | "ghost" | "default" | "raised" | null | undefined; interactive?: boolean | null | undefined; interaction?: "interactive" | "static" | "selectable" | "featured" | nul…

Card props

Type: CardProps

Values the compiler reads from the declaration.

Card props
PropTypeDefaultNotes
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—
CardHeader props2 props

CardHeader props

Type: CardHeaderProps

Values the compiler reads from the declaration.

CardHeader props
PropTypeDefaultNotes
actions?ReactNode—Right-aligned slot for a menu, badge or icon button.
icon?ReactNode—Leading slot for an icon or avatar.

Guidelines

Do

  • Keep one clickable surface per card, and let the controls inside it stay visible rather than nesting one card in another.
  • Use a `Panel` for structure inside a surface and a `Card` for the content unit a reader acts on.
  • Set `interaction="selectable"` before you pass `selected`: a card that cannot be selected has no state to show, which is the one combination a reader gets wrong here.
  • Let the spatial effects (`SpotlightSurface`, `BorderGlow`, `TiltSurface`) wrap a card, so the base component stays calm by default.

Don't

  • Do not put a glow on a passive card: an effect that only decorative cards have is an effect that means nothing.
  • Do not nest an interactive card inside another one — two click targets in the same rectangle is an ambiguous control.
  • Do not make every card in a grid interactive on the chance that one is; the hover treatment then signals nothing at all.

Accessibility

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.

Luc UI v1.0Carddark theme