Cards & elevation
StableCards on one four-step elevation scale: lux-card, a lower lux-card-flat, lux-lift on hover and lux-float for overlays.
Every surface shares one light source straight above. In light mode depth is a crisp 1px edge plus faint shadows; in dark mode it is a lighter surface with a lit top edge, and shadows only ground it. The four steps are the --lux-shadow-1 to --lux-shadow-4 variables.
Updated View source
Four steps
/** * Four steps of one light source. lux-card is the default; add lux-card-flat to sit lower, * lux-lift to rise on hover, lux-float for things that hover over the page. */const levels = [ { cls: 'lux-card lux-card-flat', step: 'Shadow 1', name: 'Flat', use: 'Rows, chips and fields inside a card.' }, { cls: 'lux-card', step: 'Shadow 2', name: 'Card', use: 'The default for content on the page.' }, { cls: 'lux-card lux-lift', step: 'Shadow 2 → 3', name: 'Lift', use: 'Rises 2px on hover. For cards that are links.' }, { cls: 'lux-card lux-float', step: 'Shadow 4', name: 'Float', use: 'Dialogs, toasts and bars over the page.' },]
export default function SurfacesElevation() { return ( <ul className="grid w-full max-w-3xl grid-cols-2 gap-4 sm:gap-6 lg:grid-cols-4"> {levels.map((l) => ( <li key={l.name} className={`${l.cls} flex min-w-0 flex-col gap-2 rounded-2xl bg-raised p-4 sm:p-5`}> <span className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase">{l.step}</span> <span className="text-[15px] font-semibold tracking-[-0.01em] text-fg">{l.name}</span> <span className="text-[13px]/5 text-pretty text-fg-2">{l.use}</span> </li> ))} </ul> )}Usage
// Once, in your app entry: Tailwind, the brand tokens, the effects and the finishing layerimport '@rchursin/product.css'<article className="lux-card rounded-2xl bg-raised p-6">…</article>Examples
Cards that are links
The whole card is the link: it lifts on hover and shows the focus ring from the keyboard.
import { Icon } from '@rchursin/product/icons/Icon'import { Link } from '@rchursin/product/lib/router'
/** * Cards that are links lift toward you on hover (mouse only; the 2px rise only when motion is * allowed). The whole card is the link, and focus shows the ring. */const pages = [ { to: '/library/finishing/metal', title: 'Metal finish', body: 'Graphite and pearl buttons with a silver rim.' }, { to: '/library/finishing/hairlines', title: 'Hairlines', body: 'Rules that fade out at both ends.' }, { to: '/library/finishing/arrivals', title: 'Scroll arrivals', body: 'Content that rises in as it scrolls into view.' },]
export default function SurfacesLift() { return ( <ul className="grid w-full max-w-3xl gap-4 sm:grid-cols-3"> {pages.map((p) => ( <li key={p.to} className="flex"> <Link to={p.to} className="lux-card lux-lift group flex w-full flex-col gap-2 rounded-2xl bg-raised p-5 focus-ring"> <span className="flex items-center justify-between gap-3 text-[15px] font-semibold tracking-[-0.01em] text-fg"> {p.title} <Icon name="arrow-right" className="size-4 shrink-0 text-fg-3 transition-transform group-hover:text-fg motion-safe:group-hover:translate-x-0.5" /> </span> <span className="text-[13px]/5 text-pretty text-fg-2">{p.body}</span> </Link> </li> ))} </ul> )}Nesting
Flat rows inside a card, one step lower than the card they sit on.
import { useId } from 'react'import { SampleBadge } from '@rchursin/product/shell/layout'
/** * Surfaces nest: a card holds flat rows. Each step down is one shadow step lower, so the * light stays consistent and nothing floats by accident. */const agents = [ { name: 'test-fixer', runs: 214, state: 'Active' }, { name: 'release-notes', runs: 38, state: 'Active' }, { name: 'db-migrate', runs: 0, state: 'Never run' },]
export default function SurfacesNested() { const titleId = useId() return ( <section aria-labelledby={titleId} className="lux-card flex w-full max-w-md flex-col gap-4 rounded-2xl bg-raised p-5"> <div className="flex items-center justify-between gap-3"> <h3 id={titleId} className="text-[15px] font-semibold tracking-[-0.01em] text-fg"> Connected agents </h3> <SampleBadge /> </div> <ul className="flex flex-col gap-2"> {agents.map((a) => ( <li key={a.name} className="lux-card lux-card-flat flex items-center justify-between gap-3 rounded-xl bg-raised px-3.5 py-2.5 text-[13px]"> <span className="min-w-0 truncate font-mono text-fg">{a.name}</span> <span className="flex shrink-0 items-center gap-3 text-fg-3"> <span className="tabular-nums">{a.runs} runs</span> <span className={a.runs ? 'text-fg-2' : undefined}>{a.state}</span> </span> </li> ))} </ul> </section> )}API
CSS
| Class or variable | What it does |
|---|---|
lux-card | The default surface: --lux-surface ground and --lux-shadow-2. In dark mode it adds a top-lit gradient and a hairline that is brightest along the top edge. It is positioned and isolated, so effects can sit inside it. |
lux-card-flat | One step lower (--lux-shadow-1). It only sets the shadow, so use it with lux-card or give it a background. |
lux-lift | With a mouse, hover raises it to --lux-shadow-3 and moves it up 2px (the move only when motion is allowed). For cards that are links. |
lux-float | The top step (--lux-shadow-4), for dialogs, toasts, menus and bars that float over the page. |
--lux-shadow-1 … 4 | The four steps as theme-aware box-shadow values, for anything else that should sit on the same scale. |
--lux-surface | The card ground: white in light mode, #121212 in dark mode. A bg-* colour utility replaces it, but in dark mode lux-card still paints its #161616 → #101010 gradient on top. |
Accessibility
- Cards are drawn with tone and shadow only. Give a card a heading when it groups content, and use a list when cards are a set.
- lux-lift reacts to a mouse only and is not a focus style. A card that is a link should be one <a> with focus-ring.
- The edge is part of the box-shadow, and forced-colours mode (Windows high contrast) removes shadows. Add a transparent border to a card that must keep an outline there.
Guidelines
Do
- Use lux-card for content, lux-card-flat for rows inside it and lux-float only for things over the page.
- Put lux-lift on cards that are links, and make the whole card the link.
- Keep bg-raised beside lux-card, as the kit does: it is the same white in light mode, and the dark gradient starts at the same #161616.
Don’t
- Don’t lift cards that do nothing when clicked.
- Don’t stack a float inside a float.
- Don’t add a border for the edge: it is already in the shadow.
In use
Used on this site
Related
- Shape & depthCorner sizes from 4 to 24 px plus the 22 px app-icon tile, four elevation steps, and the brand’s Lift shadow.
- Pointer lightA soft pool of light that follows the mouse over cards, and a lens that shows a brighter copy of a background.
- HairlinesA 1px rule that fades out at both ends, and a lit top edge for dark panels.
- DialogA modal on the native <dialog> element: the page behind goes inert, Esc closes it, and focus goes back to what opened it.
Source
The component’s own files, exactly as they run on this site.
/* RChursin_ finishing layer ("luxe"), ported from the RChursin_ UI kit (rchursin/styles/luxe.css). Needs the --ease-lux tokens in brand.css. Site changes would be marked "Site change:"; there are none, so `node .claude/skills/porting-ui-kit/scripts/kit.ts diff` should report this file in sync. */
/* RChursin_ · the finishing layer ("luxe")
The details that make the kit feel made, not generated: layered light on buttons, hairlines that catch the light, surfaces that lift, a whisper of film grain, and motion that is quick to answer and slow to arrive.
Rules this file follows - One light source, straight above: highlights on top edges, shades below. - Depth comes from hairlines and surface tone first; shadows stay faint. - Clicks answer in ≤200ms. Ambient arrivals take 600–1100ms. Exits are faster than entrances. - Movement only runs when the visitor allows motion; colour and opacity changes stay. - Nothing loops forever on its own. Everything sits in @layer components, so Tailwind utilities on the same element still win.*/