Core
Primary is the anchor
Settings

How the five derived colors are placed around the anchor.

Theme builder

Cards

Composing surface, content, and action into card layouts

Cards group related content and optional actions onto a raised surface. They are a composition pattern — not a primitive — built entirely from semantic tokens and the Button / ButtonLink primitives.


Basic card

A card with a header, body text, and no action.

Project overview

A summary of your current project status, recent activity, and upcoming milestones.


Anatomy

A card is divided into three regions:

Text
┌─────────────────────────────────────┐
│  Header  — title + optional eyebrow │
├─────────────────────────────────────┤
│  Body    — main content / media     │
├─────────────────────────────────────┤
│  Footer  — actions or metadata      │
└─────────────────────────────────────┘

Surface tokens used in a card:

RegionToken
Backgroundbg-white
Borderborder-dark/12
Drop shadowshadow-sm (shadow-md on hover)
Header / footer dividersborder-dark/12

Spacing:

RoleClass
Inner paddingp-6
Header–body / body–footer gappb-4 on the body, py-4 on the footer
Card corner radiusrounded-xl

Card with action

Place actions in the footer, separated from the body by a top border. Use Button for in-page actions and ButtonLink for navigation.

Deploy to production

Your build passed all checks. Review the diff before deploying.


Card grid

Use CSS grid with auto-fit columns so the layout adapts from one column on narrow viewports to three on wide ones.

Storage

4.2 GB

of 10 GB used

Requests

128k

last 30 days

Uptime

99.9%

over 90 days


Interactive card

A fully clickable card wraps its primary content in an <a> element (the “stretched link” technique). The anchor fills the card via position: absolute; inset: 0, and any secondary inline actions use position: relative with a higher z-index to remain independently clickable.

Guide

Getting started with tokens

Learn how primitive, semantic, and component tokens work together in Koval Core.

Stretched link rules:

  • The <a> covers the card with after:absolute after:inset-0, so no separate <style> block is needed. The card parent carries relative, and after:rounded-xl keeps the hit area on the card’s own corners.
  • focus-visible:outline-none on the anchor moves the ring to focus-visible:after:outline-2 …, so it draws around the whole card rather than around the title text. This is the one place the outline moves — it is never removed.
  • Keep the link text meaningful — it becomes the accessible name for the entire card click target.
  • Any secondary actions inside the card (e.g. a “Bookmark” icon button) must use position: relative and a higher z-index so they sit above the stretched link and remain independently activatable.
  • Do not nest another <a> or <button> inside an interactive card without isolating it with position: relative; z-index: 1.

On inverse surfaces

Use background="dark" on ComponentPreview or apply data-on-inverse to the wrapping element. Replace surface and border tokens with their inverse counterparts; button children inherit inverse styling from that surface context, with appearance="inverse" reserved for isolated controls that cannot inherit it.

Invite your team

Share access and collaborate on projects in real time.


Token reference

ClassRoleWhat it resolves to
bg-whiteCard background (light surface)Tailwind white — a card is white on the light page ground
bg-white/8Card background on inverse14% white composited over dark
border-dark/12Card border on light surface12% dark
border-white/28Card border on inverse surface28% white
rounded-xlCard corner radiusThe surface tier of the radius convention
p-6Card inner padding24px
px-6 py-4Footer padding24px / 16px
gap-8Gap between cards in a gridThe spacing convention’s grid gap
mb-[0.35em]Title → its own body textem, so it tracks the type

Radius, padding and gap come from theme-rules.md, not from a token — they are Tailwind’s own utilities, chosen once. Retheme the project and these change with it.

A card carries no border and no shadow. It is bg-white on the light page ground, and the ground change is the boundary — that is the whole zoning convention, and a card is where it is easiest to see. The one hairline below is a footer rule, which is the documented exception: two zones on the same ground that genuinely cannot be told apart otherwise. Turn borders or elevation on in the theme builder and every card gains them together.


Accessibility

Heading hierarchy

Every card that contains a visible title should use a heading element (<h2><h4>) whose level fits the surrounding page outline. Do not skip heading levels. A card grid of three peer cards should all use the same heading level.

  • Use <h2> for top-level section cards on a page with a single <h1>.
  • Use <h3> for cards nested inside a named section.
  • Use <p> (not a heading) for metadata-only cards (stats, KPI tiles) where the label is decorative rather than structural.

The class is a separate decision from the tag. A card title is an <h3> because of where it sits in the page outline, and text-h5 because of how much weight it should carry — a card title is not a section heading. Do not let one choose the other, and never leave a card title with no role class at all: .prose does not reach inside a preview, so a bare <h3> renders at body size.

Interactive card keyboard access

For the stretched-link pattern:

  • The <a> element inside the card must be focusable and have a meaningful accessible name (its text content).
  • The ::after pseudo-element that covers the card must not itself be a separate focusable element — only the <a> receives focus.
  • Visually indicate focus with focus-visible:after:outline-2 focus-visible:after:outline-offset-2 focus-visible:after:outline-primary, on the ::after pseudo-element, so the ring follows the full card boundary.
  • If the card contains secondary actions (icon buttons, “Bookmark”, “More”), each must be independently focusable and have its own aria-label. Stack order: the stretched link sits below (z-index: 0), secondary actions sit above (z-index: 1).

Color and contrast

  • Never use color as the sole differentiator between card states (e.g. selected vs unselected). Pair color with a border-width change, checkmark icon, or aria-pressed / aria-selected.
  • Text inside cards must meet WCAG AA contrast on their respective surface tokens: text-dark on bg-white and text-white on bg-dark.