Composition over configuration
A Button with an asChild prop is more useful than a Button with twenty variant props. Fewer, more composable primitives produce better outcomes than comprehensive single-component APIs.
Design system
This portfolio is its own design system — the same token architecture, accessibility standards, and component governance applied at enterprise scale across 200+ AEM templates. This page shows the system working.
2-layer
Token architecture
WCAG AA
All components
0
Axe violations
100%
:focus-visible coverage
The problem
Without a shared language, every team solves the same problems differently. Inconsistency compounds. Accessibility drifts. The codebase becomes a record of individual decisions rather than a system.
The approach
A two-layer token system: raw palette values in one layer, semantic purpose in another. Components reference only the semantic layer — the theme swap happens in the token definitions, nowhere else.
The outcome
Zero axe-core violations. WCAG 2.1 AA on every component. Light and dark mode via a single token layer — no per-component dark: overrides needed.
Architectural decisions
Semantic tokens over direct palette references
Token architectureComponents reference purpose, not value. --color-accent maps to --brand-500 in light mode and a softer value in dark mode. The dark mode swap happens in one definition — nothing else changes.
Native HTML semantics before ARIA attributes
AccessibilityThe first rule of ARIA is don't use ARIA if a native element provides the semantic. Every component in this system starts from the correct element — button, a, input. ARIA is used for gaps, not to compensate for wrong elements.
Bounded variation — fewer options, stronger consistency
GovernanceFive spacing sizes, four button variants, three surface levels. The constraint isn't a limitation — it's what makes the system feel like a system and not a collection of components.
"A design system is not a component library. It is a set of decisions that compounds — every team that adopts it, every product that uses it, every engineer who follows its patterns instead of inventing their own."
Foundation
A design system is not a component library. It is a shared language — a set of decisions that compounds across every team, every product, and every engineer who touches the codebase.
A Button with an asChild prop is more useful than a Button with twenty variant props. Fewer, more composable primitives produce better outcomes than comprehensive single-component APIs.
The first rule of ARIA: don't use it if a native element provides the semantic. Getting to a correct baseline means starting with the right element, not adding attributes to compensate for using the wrong one.
Unlimited flexibility produces inconsistency at scale. Five named sizes and four button variants aren't limitations — they're the mechanism that makes a system legible to the next engineer who uses it.
Every hardcoded value is a future inconsistency. Every token reference is a future-proofed decision. The discipline of never using arbitrary values is what makes a design system maintainable across years and teams.
Token architecture — two-layer semantic system
Token architecture — two-layer system
Layer 1
Raw values
Immutable palette. Never referenced directly in components.
--brand-500: #8C82FFLayer 2
Semantic aliases
Named by purpose, not value. Dark mode swaps these — not the raw palette.
--color-accent: var(--brand-500)Layer 3
Component tokens
Components reference semantic tokens. Tailwind @theme maps them to utilities.
bg-accentColor system
One accent prevents competition. Warm-tinted neutrals feel more human than cool grays. Every pairing meets WCAG 2.1 AA contrast requirements.
Brand palette
Brand 50
#EEEEF8
background only
Brand 100
#CECBF6
tint/muted
Brand 200
#AFA9EC
decorative
Brand 300
#9490E0
large text on white
Brand 400
#7B7BC8
3.7:1 on white
Brand 500
#4B4B8F
AA on white ✓
Brand 600
#3A3A72
AA on white ✓
Brand 700
#2D2D52
AAA on white ✓
Brand 800
#1E1E38
AAA on white ✓
Brand 900
#0B0B1A
AAA on white ✓
Neutral palette — warm-tinted
Neutral 50
#FAFAF8
page background
Neutral 100
#F5F4F0
surface
Neutral 150
#ECEAE4
border subtle
Neutral 200
#E2E0D9
border default
Neutral 300
#C8C5BC
border strong
Neutral 500
#888480
muted text
Neutral 700
#3D3A36
AA body text ✓
Neutral 900
#0F0E0D
AAA heading text ✓
Theme-aware semantic tokens
--color-bg--color-surface--color-surface-raisedToggle light/dark to see theme adaptation
Semantic color tokens — components reference only these, never raw values
| Token | Value | Usage |
|---|---|---|
--color-bg | var(--neutral-50) | Page background |
--color-surface | var(--neutral-100) | Card, panel, input background |
--color-border | var(--neutral-200) | Default border |
--color-border-strong | var(--neutral-300) | Emphasis border |
--color-text-primary | var(--neutral-900) | Headings, key labels |
--color-text-secondary | var(--neutral-700) | Body copy, descriptions |
--color-text-tertiary | var(--neutral-500) | Captions, disabled, meta |
--color-accent | var(--brand-500) | Primary interactive color |
--color-accent-hover | var(--brand-600) | Accent on hover |
--color-accent-subtle | var(--brand-50) | Tinted accent backgrounds |
Typography system
Fraunces (display) for headlines — craft and editorial weight. Outfit (sans) for UI and body — legibility and precision. Geist Mono for code and annotations.
Display / Fraunces / 72px
Aa
Heading / Outfit / 30px / medium
Senior UX Engineer
Body / Outfit / 17px / regular
Specializing in design systems, accessible interfaces, and frontend architecture at enterprise scale.
Mono / Geist Mono / 11px
Design Systems · AEM · WCAG 2.1 · TypeScript
Live type specimen — no screenshots
Type scale — 1.25× modular ratio (Major Third)
text-display72pxHero headlines
text-hero60pxSection display heads
text-4xl48pxPage titles
text-3xl36pxSection headings
text-2xl30pxSub-section heads
text-xl24pxCard titles, UI heads
text-lg20pxLead body text
text-base16pxDefault body copy
text-sm14pxSecondary copy, labels
text-xs12pxCaptions, metadata
text-2xs10pxEyebrows, badges
Spacing + shape
All spacing is a multiple of 4px. No magic numbers. The radius scale maps to semantic use cases — chips, buttons, cards, containers — so the right radius is always obvious.
Spacing scale
--space-14px--space-28px--space-312px--space-416px--space-624px--space-832px--space-1248px--space-1664px--space-2496px--space-32128pxRadius scale
--radius-xs2px--radius-sm4px--radius-md8px--radius-lg12px--radius-xl16px--radius-2xl20px--radius-full9999pxComponent ecosystem
Correct semantics, keyboard navigation, focus management, and ARIA only where native HTML falls short. These run in the browser — not screenshots.
Buttons — 4 variants
Keyboard accessible · :focus-visible · active:scale(0.97)
Badges — status variants
Semantic color — status only, never decorative
Form inputs — all states
Enter a valid email address
Label association · :focus-visible · aria-invalid
Tabs — keyboard navigable
role=tablist · aria-selected · ← → keyboard
Card — hover elevation
Design system
Token-driven, theme-aware, accessible by construction.
shadow-xs → shadow-lg · border transition · -translate-y-0.5
Focus states — :focus-visible only
Tab to see focus rings · Mouse clicks never show ring
Accessibility standards
Accessibility is an engineering responsibility, not a design review checklist. Every component is built accessible — not retrofitted.
Accessibility validation pipeline — per-component protocol
All text meets 4.5:1 ratio. Headings meet 3:1. Verified with brand-500 (#4B4B8F) at 5.2:1 on white.
Focus indicators and interactive UI components meet 3:1 against adjacent colors.
All interactive components reachable and operable via keyboard without mouse dependency.
2px outline with 3px offset applied globally via :focus-visible. Never outline: none without replacement.
prefers-reduced-motion respected globally in CSS and in Framer Motion via useReducedMotion().
All interactive components have accessible names. ARIA used only where native HTML semantics are insufficient.
Semantic HTML throughout. <nav>, <main>, <article>, <section> landmarks. <dl> for key-value content.
Skip navigation link as first focusable element on every page. Targets #main-content with tabIndex={-1}.
Motion system
Every animation answers 'what changed?' not 'isn't this cool?' Duration tokens prevent arbitrary timing. The toggle simulates prefers-reduced-motion: reduce.
Card elevation on hover · var(--duration-normal)
Section scroll reveal · var(--duration-slow)
Button press feedback · var(--duration-instant)
Motion tokens — duration and easing
| Token | Value | Usage |
|---|---|---|
--duration-instant | 80ms | Button press, micro-interactions |
--duration-fast | 150ms | Hover states, tooltip appear |
--duration-normal | 250ms | Element enter/exit |
--duration-slow | 400ms | Page reveals, section entrance |
--duration-crawl | 600ms | Deliberate hero animations |
--ease-out | cubic-bezier(0,0,0.2,1) | Elements entering viewport |
--ease-in | cubic-bezier(0.4,0,1,1) | Elements leaving viewport |
--ease-inout | cubic-bezier(0.4,0,0.2,1) | Repositioning, layout shifts |
The system doesn't just look consistent. It behaves consistently — accessible by construction, responsive by default, themeable with a single token swap.
WCAG AA
Every component
0
Axe violations
100%
:focus-visible coverage
2-layer
Token architecture