Foundations
Deck’s token architecture has three layers. Components only ever reference the middle one.
Primitive tokens Semantic tokens Component usage
--deck-primitive-* → --deck-color-action-* → .deck-button--primary
#2e6e5b action/primary background-color
Never reference a primitive from a component. Semantic tokens are what resolve differently between light and dark mode; a primitive is a fixed value.
Brand
MeetCard’s mark and wordmark, exported as Mark and Wordmark. Fixed brand
colors (Signal Green + Ink), not semantic tokens — the logo stays MeetCard
green in both themes rather than adapting like body content does.
Mark
Wordmark
This is the “Full Color — Light” variant: Ink-colored type needs a light
backdrop to read. No dark or mono variant has been supplied yet, so avoid
placing Wordmark directly on a dark or solid-brand surface. Mark alone
(just the two green card shapes, no type) is safer on darker surfaces — it’s
what AppBar’s stories use in the app shell.
import { Mark, Wordmark } from '@meetcard/deck'
<AppBar brand={<Mark style={{ height: 24, width: 24 }} />} />
Deck’s own identity vs. MeetCard’s
This Storybook’s own chrome (its favicon and sidebar logo, in
.storybook/theme.ts and public/favicon.svg) uses a separate mark and
wordmark for Deck itself — a stack of three cards, distinct from the
Mark/Wordmark above. That’s deliberate: this Storybook documents Deck the
tool, so its chrome carries Deck’s own identity, while Mark/Wordmark
(above) are MeetCard’s product brand, meant for actual product UI. Deck’s own
assets live in .storybook/brand/ and aren’t part of the public API — there
was no product need to export them as components.
Color
Sourced from Figma → Brand guidelines → MeetCard - Brand Colors (July 2026). Light values are exact; dark values are derived and verified to clear WCAG AA (4.5:1 body text, 3:1 UI boundaries) against their own surface.
background
text
border
action
status
Two deliberate dark-mode inversions
- Slate becomes the elevated surface. The page sits deeper than Slate, so cards read as raised rather than flush.
- Solid fills flip their on-color. In dark mode
action/primaryandaction/destructiveare bright with Ink text, because white on a usable-lightness green cannot reach 4.5:1.
Typography
Inter, across four semantic scales. All sizes in px, aligned to a 4px baseline.
display
heading
body
label
Display serif
--deck-font-serif (Fraunces) is a second typeface reserved for signature,
editorial moments — today that’s just the name on PersonCard. It is opt-in
via <Heading family="serif">, never the default, and like Inter, Deck does
not bundle or fetch it: load Fraunces however your app loads fonts, and the
stack degrades to Georgia.
Spacing
Base 4px, Tailwind-aligned. Use via Stack’s gap, or var(--deck-space-N).
Radius
lg (8px) is the default for cards, buttons, and inputs. full is for
avatars, badges, and pill buttons.
Elevation
Applied per component, never page-level.
xs
focus rings, inputssm
cards, menusmd
panels, popoverslg
modals, drawersxl
command palette2xl
sheets, overlaysMotion & breakpoints
Motion and breakpoints are not yet specified in the Figma kit — the values below are Deck defaults and should be replaced when design publishes them.
| Token | Value | Use |
|---|---|---|
--deck-duration-fast |
120ms | Hover, color changes |
--deck-duration-base |
200ms | Elevation, transforms |
--deck-duration-slow |
320ms | Entrances, overlays |
--deck-ease-standard |
cubic-bezier(0.2, 0, 0, 1) |
Default |
All durations collapse to 0ms under prefers-reduced-motion: reduce.
Breakpoints are sm 640, md 768, lg 1024, xl 1280, 2xl 1536. CSS
cannot read a custom property inside a media query, so JS consumers should use
the mediaQuery helper:
import { mediaQuery } from '@meetcard/deck'
window.matchMedia(mediaQuery('md')) // "(min-width: 768px)"
Checking a component responsively
Use Viewport size in the toolbar above the canvas. It carries device
widths for “how does this look on a phone”, and Edge · pairs for “does the
breakpoint fire correctly”.
The control only appears on a story, not on a Docs page. A Docs page renders many stories at once, so there is no single canvas to resize — if you clicked a component name in the sidebar you landed on Docs, and the control is genuinely absent. Open any story beneath it and it appears.
The edge presets straddle rather than sit on a breakpoint, because Deck’s
queries are max-width: at exactly 640px the mobile rule matches, so a
preset named “sm 640” would show the narrow layout — the opposite of what
you’d expect. sm − 1px and sm + 1px show each side honestly.
Two things worth knowing while you’re in there:
- Only
sm640 andlg1024 are actually queried in CSS.md768,xl1280 and2xl1536 exist as tokens but no rule references them, so they are offered as device widths only. - At exactly 640px,
Sheetwidens (min-width: 640px) while every other component is still narrow (max-width: 640px). Both rules match on the line. Nothing is broken, but that width is not a state any real screen sits in.