React Composition Layer
How Next.js consumers wrap BDS tokens with typed primitives, style presets, and a shared markdown renderer.
The CSS cascade gives every component access to BDS tokens via var(--...). The React composition layer is the consumer-side convention for referencing those tokens from TSX without inlining var() strings everywhere. Three files, one rule.
Never write raw var() strings in a style prop. Always import from the typed token / style layer. This is what keeps theme switching, dark mode, and per-client brand overrides working without component rewrites.
The three files
src/lib/tokens.ts — typed primitives (one var() per leaf)
src/lib/styles.ts — composed CSSProperties presets (text.body, heading.section, ...)
src/components/prose.tsx — single ReactMarkdown configurationReference implementation: brik-client-portal — copy these three files when starting a new app project, then point your globals.css at the installation cascade.
1. src/lib/tokens.ts — typed primitives
Maps Figma style names to CSS-custom-property references. One leaf = one var(). These reference the property (not the resolved value), so they work correctly with Mode switches and per-client Brand Kit overrides emitted into @layer client-theme.
export const font = {
family: { body: 'var(--_typography---font-family--body)', /* ... */ },
size: {
body: { xs: 'var(--_typography---body--xs)', sm: 'var(--_typography---body--sm)', md: 'var(--_typography---body--md-base)' /* ... */ },
label: { sm: 'var(--_typography---label--sm)', md: 'var(--_typography---label--md-base)' /* ... */ },
heading: { small: 'var(--_typography---heading--small)', medium: 'var(--_typography---heading--medium)' /* ... */ },
},
lineHeight: { tight: 'var(--font-line-height--100)', normal: 'var(--font-line-height--150)' /* ... */ },
weight: { light: 300, regular: 400, medium: 500, semibold: 600, bold: 700 },
} as const;
export const color = {
text: { primary: 'var(--_color---text--primary)', secondary: 'var(--_color---text--secondary)', muted: 'var(--_color---text--muted)' /* ... */ },
surface: { primary: 'var(--_color---surface--primary)' /* ... */ },
border: { default: 'var(--_color---border--default)', muted: 'var(--_color---border--muted)' /* ... */ },
} as const;
export const space = { none: 'var(--_space---none)', xs: 'var(--_space---xs)', sm: 'var(--_space---sm)', md: 'var(--_space---md)', lg: 'var(--_space---lg)' /* ... */ } as const;
export const gap = { none: 'var(--_space---gap--none)', xs: 'var(--_space---gap--xs)', sm: 'var(--_space---gap--sm)', md: 'var(--_space---gap--md)' /* ... */ } as const;
export const border = {
width: { sm: 'var(--_border-width---sm)' /* ... */ },
radius: { sm: 'var(--_border-radius---sm)' /* ... */ },
} as const;2. src/lib/styles.ts — composed presets
Multi-property CSSProperties objects built from tokens. These map 1:1 to Figma text styles so a designer's spec translates directly.
import type { CSSProperties } from 'react';
import { font, color, space, gap } from './tokens';
export const text = {
body: { fontFamily: font.family.body, fontSize: font.size.body.md, lineHeight: font.lineHeight.normal, color: color.text.primary } satisfies CSSProperties,
bodySmall: { fontFamily: font.family.body, fontSize: font.size.body.sm, lineHeight: font.lineHeight.normal, color: color.text.secondary } satisfies CSSProperties,
bodyXs: { fontFamily: font.family.body, fontSize: font.size.body.xs, lineHeight: font.lineHeight.normal, color: color.text.muted } satisfies CSSProperties,
} as const;
export const heading = {
section: { fontFamily: font.family.heading, fontSize: font.size.body.lg, fontWeight: font.weight.semibold, color: color.text.primary, margin: `0 0 ${space.md}` } satisfies CSSProperties,
subsection: { fontFamily: font.family.heading, fontSize: font.size.body.md, fontWeight: font.weight.semibold, color: color.text.primary, margin: `${space.md} 0 ${gap.sm}` } satisfies CSSProperties,
} as const;
export const label = {
subtitle: { fontFamily: font.family.label, fontSize: font.size.label.sm, fontWeight: font.weight.semibold, color: color.text.secondary, textTransform: 'uppercase' as const, letterSpacing: '0.05em' } satisfies CSSProperties,
} as const;
export const meta = {
label: { ...label.subtitle, margin: `0 0 ${gap.xs}` } satisfies CSSProperties,
value: { ...text.body, margin: 0 } satisfies CSSProperties,
} as const;
export const list = {
ul: { margin: `0 0 ${gap.md}`, paddingLeft: space.lg, fontFamily: font.family.body, fontSize: font.size.body.md, lineHeight: font.lineHeight.normal, color: color.text.secondary } satisfies CSSProperties,
li: { marginBottom: gap.xs } satisfies CSSProperties,
} as const;3. src/components/prose.tsx — shared markdown renderer
Single ReactMarkdown configuration for any markdown content in the app. Built from the style presets.
'use client';
import ReactMarkdown from 'react-markdown';
import { text, heading, list } from '@/lib/styles';
import { font, color, space, gap } from '@/lib/tokens';
export function Prose({ content }: { content: string }) {
return (
<div style={text.body}>
<ReactMarkdown components={{
h2: ({ children }) => <h2 style={{ ...heading.section, margin: `${space.lg} 0 ${gap.sm}` }}>{children}</h2>,
h3: ({ children }) => <h3 style={heading.subsection}>{children}</h3>,
p: ({ children }) => <p style={{ margin: `0 0 ${gap.md}` }}>{children}</p>,
ul: ({ children }) => <ul style={list.ul}>{children}</ul>,
ol: ({ children }) => <ol style={{ ...list.ul, listStyleType: 'decimal' }}>{children}</ol>,
li: ({ children }) => <li style={list.li}>{children}</li>,
strong: ({ children }) => <strong style={{ fontWeight: font.weight.semibold }}>{children}</strong>,
hr: () => <hr style={{ border: 'none', borderTop: `var(--_border-width---sm) solid ${color.border.muted}`, margin: `${space.lg} 0` }} />,
}}>{content}</ReactMarkdown>
</div>
);
}Pre-commit hook — token compliance gate
Block hardcoded pixel sizes and line-heights from landing in .tsx / .ts files. Add to .husky/pre-commit after lint:
# Check staged .tsx/.ts files for hardcoded values that should use BDS tokens
STAGED_SRC=$(git diff --cached --name-only --diff-filter=ACM -- 'src/**/*.tsx' 'src/**/*.ts' | grep -v 'email\.ts' || true)
if [ -n "$STAGED_SRC" ]; then
HARDCODED_PX=$(echo "$STAGED_SRC" | xargs grep -n "fontSize: '[0-9]" 2>/dev/null || true)
HARDCODED_LH=$(echo "$STAGED_SRC" | xargs grep -n "lineHeight: [0-9]" 2>/dev/null | grep -v "lineHeight: 'var(" || true)
if [ -n "$HARDCODED_PX" ] || [ -n "$HARDCODED_LH" ]; then
echo "Token compliance: hardcoded values found in staged files."
echo " Use imports from @/lib/tokens or @/lib/styles instead."
[ -n "$HARDCODED_PX" ] && echo "$HARDCODED_PX"
[ -n "$HARDCODED_LH" ] && echo "$HARDCODED_LH"
echo " Run 'npm run audit:tokens' for full report."
exit 1
fi
fiUsage patterns
// Import primitives for individual values
import { font, color, space, gap, border } from '@/lib/tokens';
// Import composed presets for multi-property styles
import { text, heading, label, meta, list } from '@/lib/styles';
// Import the shared markdown renderer
import { Prose } from '@/components/prose';
// Use a preset directly
<p style={text.body}>Default body text</p>
// Override one property from a preset
<p style={{ ...text.body, color: color.text.muted }}>Muted body</p>
// Combine preset + token
<div style={{ ...heading.section, margin: `0 0 ${space.lg}` }}>
// Render markdown content
<Prose content={markdownString} />Figma style → code mapping
| Figma style | Import | Usage |
|---|---|---|
| body/md (16/150) | text.body | Default body text |
| body/sm (14/150) | text.bodySmall | Secondary text |
| body/xs | text.bodyXs | Fine print |
| subtitle/md (uppercase) | label.subtitle | Section labels |
| Section heading (18px) | heading.section | h2 inside cards |
| Sub-heading (16px) | heading.subsection | h3 inside cards |
| Label + value pair | meta.label / meta.value | Detail pages |
| List items | list.ul / list.li | Bullet lists |
| Markdown content | <Prose content={md} /> | Any markdown block |
Related
- Installation — wire the token cascade in
globals.css - The Cascade — two-tier × four-layer × modes architecture
- Framework Guides — Next.js / Astro / Webflow setup notes
- Primitives → Color — semantic color vocabulary
- Utilities — BDS-side tooling (Style Dictionary build pipeline,
bds-findCLI, Adopt / Extend / Graduate cascade)