Cards
components specs/components/cards.kmd
Card — a container of related content + actions, typically clickable as a whole. Material parity (`/components/cards`). 3 variants (elevated, filled, outlined), uniform anatomy, and rules for composition with images, lists, and feeds.
When this spec applies
Primary triggers
- Add a card anywhere in Koder UI
All triggers
- Group related content into a discrete unit
- Build a feed of items (per `canonical-layouts.kmd` L1)
- Render a product/article/profile tile
Specification body
Spec — Cards
Facet Visual do Koder Design. Material parity: https://m3.material.io/components/cards.
3 card variants
| Variant | Visual | Use |
|---|---|---|
| Elevated | Surface bg + shadow level 1 | Default — slight depth, draws attention |
| Filled | Surface-variant bg + no shadow | Less emphasis; for embedded cards within a denser context |
| Outlined | Transparent bg + 1px accent border | High structure, low visual weight (forms with grouped fields) |
Anatomy
┌──────────────────────────────┐
│ [Optional media] │ ← image / video / illustration
│ │
│ Title (title-large) │
│ Subtitle (body-medium muted)│
│ │
│ Body content │
│ │
│ [Action 1] [Action 2] │ ← optional actions
└──────────────────────────────┘
- Container: radius-md, padding 16-24 px
- Media (optional): full-width image, aspect 16:9 default
- Title:
title-large(22/28, weight 600) - Subtitle:
body-medium(14/20) intext-muted - Body:
body-medium - Actions: button row, right-aligned (or left in RTL)
R1 — Card density
| Density | Padding | Use |
|---|---|---|
| Comfortable | 24 px | Default — feeds, dashboards |
| Compact | 16 px | Dense lists (table cards, multi-column grids) |
| Spacious | 32 px | Hero cards (single card per fold) |
Density follows themes/ui-style.kmd preset's density token; can be
explicitly overridden per card via class modifier.
R2 — Clickable card
Most cards are clickable as a whole (navigate to detail).
- Entire card surface is the hit area
- Hover state: subtle elevation boost (+1 level) OR background tint
- Focus ring: 2 px on outer container (per
interaction/states.kmd) role="link"if it navigates;role="button"if it triggers an action- Inner buttons are stop-propagation: clicking them doesn't trigger the card's main action
- Cursor: pointer on whole card
R3 — Compositions
Feed item card
Used in vertical scrollable feeds (per canonical-layouts.kmd L1).
- Compact density
- Title + subtitle + 1-3 lines of body
- Optional avatar (40 px) + author name + timestamp
- Click navigates to detail
Product card
- Elevated variant
- Hero image (16:9 or 1:1)
- Title + price + rating
- "Add to cart" or similar primary action
- Hover: shadow boost + slight scale (1.02)
List-detail card
- Outlined or Filled variant
- Selected state: accent-tinted bg + accent border (1.5 px)
- Used in list-detail layout (per L2)
Stats card (dashboard)
- Outlined variant
- Large numeric value (
headline-large) - Label (
title-smallmuted) - Optional sparkline / mini-chart
R4 — Cards within cards
Anti-pattern. A card MUST NOT contain another card. If nesting feels needed, the inner content should be a list-item, expansion-panel, or embedded section — not a card.
Exception: a card containing one image-card preview (e.g., a tweet embed inside a notification card) — allowed because the inner is a quoted/embedded artifact, not a peer container.
R5 — Media handling
- Aspect ratio: 16:9 (default), 1:1 (square avatar/product), 4:3 (legacy)
- Loading: skeleton placeholder matches final aspect (no layout shift)
- Failure: fallback to
surface-variantbg + icon - Alt text:
altattribute REQUIRED for non-decorative images - Lazy-loading:
loading="lazy"for cards below the fold
R6 — Per-preset variation
| Preset | Variant emphasis |
|---|---|
material3 | Elevated default (subtle shadow) |
material2 | Stronger shadows, sharper corners |
windows_11 | Mica backdrop blur on Elevated |
windows_95 | 3D bevel border instead of shadow; sharp corners |
glassmorphism | Translucent + backdrop blur |
brutalist | Outlined only (no shadows, thick border) |
neumorphism | Soft inverse shadow (looks "pressed into" surface) |
dieter_rams | Outlined default; minimal ornament |
R7 — Accessibility
- Card title is a heading (
<h2>-<h4>depending on page level) - Clickable card focus ring on outer container only (not on title)
- Inner buttons reachable by Tab (separately from card focus)
- Reading order matches visual order (DOM = visual)
R8 — Forbidden patterns
- ❌ Card without title (just a body of text) — use a
<section>instead - ❌ Multiple primary actions in card actions row (1 primary, N secondary)
- ❌ Card with no border + no shadow + no bg variation (use a plain
<div>) - ❌ Animations that change card height on hover (causes feed shift)
Cross-link
app-layout/canonical-layouts.kmd— feed (L1) uses cardsthemes/elevation.kmd— Elevated variant level 1 defaultthemes/shape.kmd— radius-md containerinteraction/states.kmd— hover/focus per card typefoundations/elements.kmd— Container family
Requirements (testable)
Requirement: Card variants render their defining visual treatment {#req-cards-variants}
A card SHALL render according to its declared variant: the Elevated variant SHALL have a surface background with shadow level 1, the Filled variant SHALL have a surface-variant background and no shadow, and the Outlined variant SHALL have a transparent background with a 1px accent border.
Scenario: Elevated variant carries shadow level 1
- GIVEN a card declared with the Elevated variant
- WHEN the card is rendered
- THEN its background resolves to the surface color
- AND a shadow of elevation level 1 is applied to the container
Scenario: Filled variant has no shadow
- GIVEN a card declared with the Filled variant
- WHEN the card is rendered
- THEN its background resolves to the surface-variant color
- AND no box shadow is applied to the container
Scenario: Outlined variant has a transparent background and accent border
- GIVEN a card declared with the Outlined variant
- WHEN the card is rendered
- THEN its background is transparent
- AND a 1px border in the accent color is drawn around the container
Requirement: Card density maps to the specified padding {#req-cards-density}
A card SHALL apply container padding matching its density token: Comfortable SHALL be 24px, Compact SHALL be 16px, and Spacious SHALL be 32px, unless explicitly overridden per card via a class modifier.
Scenario: Compact density applies 16px padding
- GIVEN a card set to Compact density
- WHEN the card is rendered
- THEN its container padding computes to 16px on all sides
Scenario: Per-card modifier overrides the preset density
- GIVEN a card whose theme preset density resolves to Comfortable (24px)
- WHEN the card carries a class modifier setting Spacious density
- THEN its container padding computes to 32px on all sides
Requirement: Clickable card exposes the whole surface with correct role and inner-button isolation {#req-cards-clickable}
A clickable card SHALL make the entire card surface the hit area, SHALL expose role="link" when it navigates or role="button" when it triggers an action, and clicking an inner button SHALL NOT trigger the card's main action.
Scenario: Whole-surface click triggers navigation with role=link
- GIVEN a clickable card that navigates to a detail view
- WHEN the user clicks a non-interactive region of the card surface
- THEN the card's main navigation action fires
- AND the card element exposes
role="link"
Scenario: Inner button click does not trigger the card action
- GIVEN a clickable card containing an inner action button
- WHEN the user clicks the inner button
- THEN the inner button's own handler fires
- BUT the card's main action does not fire
Requirement: Card media requires alt text and reserves layout space {#req-cards-media}
Card media SHALL provide an alt attribute for every non-decorative image, SHALL show a skeleton placeholder matching the final aspect ratio while loading so no layout shift occurs, and SHALL fall back to a surface-variant background with an icon on load failure.
Scenario: Non-decorative media image carries alt text
- GIVEN a card whose media is a non-decorative image
- WHEN the card is rendered
- THEN the image element has a non-empty
altattribute
Scenario: Loading placeholder reserves the final aspect ratio
- GIVEN a card with 16:9 media that has not finished loading
- WHEN the card is rendered
- THEN a skeleton placeholder occupies a 16:9 box
- AND the media box height does not change once the image loads (no layout shift)
Requirement: Card accessibility uses a heading title and a single outer focus ring {#req-cards-a11y}
A card SHALL render its title as a heading element (<h2>–<h4> per page level), a clickable card SHALL place the focus ring on the outer container only and not on the title, inner buttons SHALL remain reachable by Tab separately from the card focus, and the DOM reading order SHALL match the visual order.
Scenario: Card title is a heading element
- GIVEN a card with a title
- WHEN the card is rendered
- THEN the title is a heading element in the
<h2>–<h4>range
Scenario: Focus ring appears on the container, and inner buttons are separately tabbable
- GIVEN a clickable card that contains an inner button
- WHEN the user presses Tab to move focus onto the card
- THEN the focus ring is drawn on the outer container and not on the title
- AND a subsequent Tab moves focus to the inner button
References
specs/foundations/elements.kmdspecs/app-layout/canonical-layouts.kmdspecs/themes/shape.kmdspecs/themes/elevation.kmd