Skip to content

Card

A block container with a clean surface, hairline border, and an interactive state when the card is rendered as a link or button. Class + data-attribute API: one .x-card class plus data-variant and data-size.

Install

import "elements-kit/ui/styles/theme.css";
import "elements-kit/ui/styles/scaling.css";
import "elements-kit/ui/styles/radius.css";
import "elements-kit/ui/styles/space.css";
import "elements-kit/ui/styles/typography.css";
import "elements-kit/ui/styles/shadow.css";
import "elements-kit/ui/styles/material.css";
import "elements-kit/ui/styles/palette/gray.css";
import "elements-kit/ui/styles/neutral/gray.css";
import "elements-kit/ui/card/card.css";

unset.css is not required for <div>s. When the card is a <button> or <a>, add unset first to neutralise the native rendering before applying .x-card.

API

<div class="x-card" data-variant="surface" data-size="3">
Card content.
</div>
AttributeValues
data-variantsurface (default), elevated, borderless
data-size1, 2, 3 (default), 4, 5
data-radius (on a parent)none, small, medium, large, pill
data-material-background (on a parent)solid, translucent
data-accent (on a parent)any imported color scale β€” used by the focus ring

Sizing

data-size scales padding and border-radius together.

data-sizepaddingradius
1--space-3--radius-4
2--space-4--radius-4
3 (default)--space-5--radius-5
4--space-6--radius-5
5--space-8--radius-6

Set data-radius on an ancestor to scale corner curvature globally; --scaling on <html> scales padding and radius together.

Variants

  • surface β€” 1px hairline border (--neutral-a5). Default. The everyday card.
  • elevated β€” same border plus a tailored drop shadow. Uses dual outer-on-element + inner-on-::after shadows (--card-elevated-box-shadow-outer / -inner) with matching 6-layer counts across neutral/hover/active so the lift transitions smoothly. Use for primary, hover-able cards.
  • borderless β€” no border, no background. Hover fills with --neutral-a3. Use inside dense lists or as clickable rows.

Inset

data-inset on a direct child of .x-card makes that child bleed past the card’s padding to the card’s edge. Common case: a hero image at the top of a card.

without inset: with data-inset="top":
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β”‚
β”‚ β–‘β–‘β–‘ HERO IMAGE β–‘β–‘ β”‚ β”‚β–‘β–‘β–‘β–‘ HERO IMAGE β–‘β–‘β–‘β–‘β–‘β”‚
β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β”‚
β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Heading β”‚ β”‚ Heading β”‚
β”‚ Body β”‚ β”‚ Body β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
<div class="x-card">
<img data-inset="top" src="/hero.webp" alt="" />
<h3>Title</h3>
<p>Body</p>
</div>
ValueSideUse case
data-inset="top"block-startVertical card β€” hero at top
data-inset="bottom"block-endVertical card β€” footer media at bottom
data-inset="start"inline-start (left LTR / right RTL)Horizontal card β€” leading media
data-inset="end"inline-endHorizontal card β€” trailing media

Pick the pair that matches your card’s orientation. top / bottom are physical (block axis). start / end are logical (inline axis) and flip automatically in RTL.

The inset element rounds the corners it bleeds into and squares the others. The corner radius is computed from the card’s --card-border-radius, so it always lines up flush with the card’s border.

Surface material

The card reads var(--color-material, var(--color-surface)) for its background, so by default it sits on the theme surface color. Wrap any ancestor with data-material-background="translucent" to swap to a frosted backdrop:

<section data-material-background="translucent">
<div class="x-card">Frosted</div>
</section>

Available values: solid, translucent. translucent adds a 64px backdrop blur.

Interactive cards

When the card itself is an anchor, button, or label, hover and active states apply. Wrap the underlying element with unset to drop native styling first:

<a href="/post" class="unset x-card" data-variant="elevated">
<h3>Post title</h3>
<p>Body…</p>
</a>
Statesurfaceelevatedborderless
hoverborder bumps to --neutral-a7shadow lifts to --shadow-3fills with --neutral-a3
activeborder at --neutral-a6shadow returns to --shadow-2fills with --neutral-a4

Non-interactive cards (a plain <div>) skip these states entirely β€” no need to gate them with media queries.

Focus

When a card is focusable, focus shows a 2px outline using --focus-8. Set data-accent on an ancestor to pick the accent color for the focus ring.

<div data-accent="mint">
<a href="/post" class="unset x-card">…</a>
</div>

Theming

Light/dark flips automatically via the .dark class on a parent β€” see Light & dark. The surface picks up --color-material (translucent backdrop) or --color-surface (solid neutral), both defined in theme.css.

Accessibility

.x-card ships with no ARIA role β€” it’s a pure visual class. When the card is interactive, render it as an <a> or <button> so it picks up the correct semantics, keyboard handling, and the kit’s focus styles. Wrap nested links in a non-interactive card with care: nested interactives are an accessibility footgun.