Components · Data display & feedback
Card
Bordered structural containers. Four variants share an 8px radius and 1px border — only padding and border color change. No shadows at rest: elevation is reserved for overlays.
Preview
24px padding, hairline border, no shadow — the default container.
Tighter 8px padding for dense layouts.
Muted surface for nested or secondary panels.
High-contrast border for major notices — one per view.
Clickable card — hover and focus states.
Primary tint + primary border marks the active card.
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Card" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `card` spec (variants: card, card-compact, card-secondary, card-announcement) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `card` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
┌─ card ──────────────────────────────────────┐ ← 1px {colors.hairline} border
│ │ {rounded.base} 8px radius
│ [header region — product-defined] │
│ ───────────────────────────────── │
│ [content region — product-defined] │
│ │
│ [footer region — product-defined] │
└─────────────────────────────────────────────┘
Parts:
- Surface — carries fill, border, radius, and optional shadow
- Padding zone — uniform on all sides, set by variant
- Header / Content / Footer — layout regions; not tokenized at card level, managed by child components
| State | Background | Shadow | Notes |
|---|---|---|---|
| Default | {colors.surface-card} |
{elevation.shadow.raised} |
Standard resting card |
| Hover (interactive card) | {colors.surface-muted} |
{elevation.shadow.raised} |
Only when card itself is clickable |
| Focus (interactive card) | {colors.surface-card} + 2px solid {colors.border-focus} |
{elevation.shadow.raised} |
Keyboard only |
| Selected (interactive card) | {colors.primary-bg} |
{elevation.shadow.raised} |
border: 1px solid {colors.primary} |
| Disabled | {colors.disabled-bg} |
None | 50% opacity on content |
| Loading | Skeleton fills content area | {elevation.shadow.raised} |
— |
When to use
- Tier 1 (standard): the primary grouping container for dashboard tiles, form sections, and content panels.
- Tier 2 (compact): dense layouts where the card chrome is present but padding is owned by child components.
- Tier 3 (secondary): nested information inside a standard card, or lower-emphasis sections alongside primary cards.
- Tier 4 (announcement): one-time system-level messages, release notes, or onboarding milestones. Never for error states — use a feedback alert.
When not to use
- Do not use a card as a navigation element — it is a content container, not a link.
- Do not nest more than 2 card levels (standard containing secondary). Deeper nesting creates visual ambiguity.
- Do not use
card-announcementfor routine information — it loses its signal value if overused. - Do not apply shadows to multiple cards in the same grid — it removes the elevation differentiation.
Do
- Use consistent card variants within the same section or grid.
- Apply
{elevation.shadow.raised}only when the card must visually lift above a sunken context. - Use
card-compactwhen child components (table, list) already provide their own internal spacing. - Limit
card-announcementto one instance per view.
Don't
- Don't mix tiers arbitrarily — define which tier maps to which content pattern and apply it consistently.
- Don't put a shadow on a card that sits on
{colors.surface-card}(same elevation) — shadow requires a lower surrounding surface. - Don't add
card-announcementstyling to inline validation or feedback messages — use the feedback/alert component.
- Card headers should be short noun phrases (not sentences).
- Body content uses
{colors.body}/ 14px / 400 Geist — do not override font inside cards. - Use a separator (
{colors.hairline}, 1px) to divide header, content, and footer regions when needed. Do not use borders on child components for this purpose.
Keyboard
Card is a layout component with no keyboard behavior of its own. When used as an interactive card (clickable), wrap it in a <button> or <a> — keyboard interaction is then standard button or link behavior: Tab moves focus to the card, and Enter / Space activates it when the interactive prop is set.
Accessibility
- Non-interactive cards: no role required. The card is a layout container.
- Interactive cards: add
role="button"andtabindex="0". Setaria-labeldescribing what happens on activation. - Announcement cards: add
role="region"witharia-labeldescribing the announcement type (e.g.,aria-label="System announcement"). - Focus ring: interactive cards show 2px
{colors.border-focus}outline at 2px offset — identical to button focus. - Contrast:
{colors.hairline}border achieves 3:1 against{colors.surface-card}in light mode (WCAG AA — UI components boundary).{colors.border-announcement}achieves 3:1 minimum.
Spec
Values and token references resolved straight from the Zetta spec.
- backgroundColor
- {colors.surface-card}
- textColor
- {colors.ink}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- padding
- {spacing.page}
- shadow
- none
- backgroundColor
- {colors.surface-card}
- textColor
- {colors.ink}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- padding
- {spacing.sm}
- shadow
- none
- backgroundColor
- {colors.surface-secondary}
- textColor
- {colors.ink}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- padding
- {spacing.md}
- shadow
- none
- backgroundColor
- {colors.surface-card}
- textColor
- {colors.ink}
- borderColor
- {colors.border-announcement}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- padding
- {spacing.page}
- shadow
- none
Known gaps
- Interactive / clickable card variant (
card-interactive) — hover and focus state tokens not yet formalized. Current guidance:{colors.surface-secondary}on hover,{colors.border-focus}outline on focus. - Card header and footer sub-regions — spacing and divider treatment not tokenized at card level.
- Card loading skeleton — skeleton token exists in
zetta-feedback.md; card-level loading pattern not yet specified.