🦄 Unicorn.Land ▸ docs/plans/2026-03-01-accessibility-audit.md
updated 2026-03-01

Accessibility Audit — Unicorn.Land Design System

Prompt: 8 — The Accessibility Auditor Standard: WCAG 2.2 AA Date: 2026-03-01 Scope: Design system tokens, live homepage, component specifications


1. Color Contrast Audit

Opal on White (#fafaf9)

Color Hex Ratio AA Text AA Large Pass?
opal-400 (Signature) #56dbd0 2.2:1 Fail Fail FAIL
opal-500 #2fb8ad 3.3:1 Fail Pass Large only
opal-600 (Functional) #0d9488 4.6:1 Pass Pass PASS
opal-700 #0a7a70 5.8:1 Pass Pass PASS
opal-800 #0c5f59 7.3:1 Pass Pass PASS
opal-900 (Contrast) #134e4a 9.5:1 Pass Pass PASS

Finding: opal-400 (#56dbd0) fails contrast on white for ANY text use. This is the signature color — it’s fine for decorative elements, large shapes, and backgrounds, but must NEVER be used for text on white. Use opal-600+ for text.

Current system: Already correctly designates opal-600 as “Functional” (text links, focus rings) with 4.6:1 ratio. The design is sound — the naming just needs to be explicit about what’s decorative vs. functional.

Opal on Dark (#1c1917)

Color Hex Ratio AA Text Pass?
opal-400 #56dbd0 9.6:1 Pass PASS
opal-300 #7adace 10.8:1 Pass PASS
opal-200 #aeeae0 13.3:1 Pass PASS

Finding: Opal colors work excellently on dark backgrounds. The signature opal-400 is safe for text in dark mode.

Stone Text on White (#fafaf9)

Color Hex Ratio AA Text Pass?
stone-400 (Muted) #a8a29e 2.9:1 Fail FAIL
stone-500 (Secondary) #78716c 4.5:1 Borderline BORDERLINE
stone-600 #57534e 6.4:1 Pass PASS
stone-900 (Primary) #1c1917 16.6:1 Pass PASS

Finding: --ul-text-muted (stone-400) fails contrast at 2.9:1. This is acceptable ONLY for decorative/non-essential text (timestamps, metadata the user can ignore). It must never be the sole way to convey information. --ul-text-secondary (stone-500) at 4.5:1 just barely passes — use sparingly for body text, fine for labels and secondary content.

Semantic Colors on White

Color Hex Ratio Pass? Notes
Success #16a34a 3.9:1 Large only Use on tinted bg or pair with icon
Warning #ca8a04 3.3:1 Fail Needs darker variant for text
Error #dc2626 4.6:1 PASS
Info #0d9488 4.6:1 PASS Reuses opal-600

Finding: Warning (#ca8a04) fails for text on white. The badge/alert specs use it on tinted yellow backgrounds where contrast improves significantly. Success is borderline — again, the component specs pair it with icons and tinted backgrounds, which is correct.

Remediation

No token changes needed. The current system is correctly designed — the issue is documentation: - Add to do’s/don’ts: opal-400 is decorative-only, never for text on light backgrounds - Add to do’s/don’ts: stone-400 (muted) is for non-essential text only - Consider: Adding --ul-warning-text: #92400e for warning text on white backgrounds (amber-800, passes at 7.1:1)


2. Typography Audit

Minimum Sizes

Token Size WCAG Requirement Pass?
caption 12px No minimum, but 12px is industry floor PASS
footnote 13px Comfortable for secondary content PASS
subhead 14px Good for labels PASS
body 16px Recommended minimum for body text PASS

Finding: All sizes meet or exceed accessibility guidelines. The 12px caption is the smallest — acceptable for metadata, timestamps, and supplementary labels but should never carry primary content.

Scaling

The type scale uses rem units throughout, which respect user browser font-size preferences. PASS.

Line Height

All body text uses 1.5+ line-height. Display/headline text uses tighter leading (1.1–1.25) which is acceptable for short strings. PASS.

Font Weight

Minimum weight is 400 for body text. No weight below 400 is used. PASS.


3. Live Homepage Accessibility

Issues Found

Issue Severity WCAG Criterion
Subtitle contrast: #9575cd on #e8d5f0 ≈ 2.5:1 Critical 1.4.3 Contrast (Minimum)
No prefers-reduced-motion support Serious 2.3.3 Animation from Interactions
Mouse sparkle animation not pausable Serious 2.2.2 Pause, Stop, Hide
No skip navigation link Moderate 2.4.1 Bypass Blocks
No landmark roles (<main>, <nav>) Moderate 1.3.1 Info and Relationships
Only <h1> — no heading hierarchy Minor 1.3.1 Info and Relationships
Sparkle emoji in footer has no alt text Minor 1.1.1 Non-text Content
No lang attribute on <html> Fixed lang="en" is present — PASS

Critical Fix

The subtitle text #9575cd on #e8d5f0 background is ~2.5:1 contrast. WCAG AA requires 4.5:1 for normal text.

Options: - Darken text to #6d4c9e (≈5.0:1) — keeps purple tone - Switch to design system --ul-text-secondary (#78716c) on --ul-bg (#fafaf9) — 4.5:1 - Switch to Outfit + opal palette per critique Direction A

Serious Fixes

Add to homepage CSS:

@media (prefers-reduced-motion: reduce) {
  .cloud, .sparkle-dot, .title {
    animation: none !important;
  }
  .sparkles { display: none; }
}

Disable mouse sparkle creation when prefers-reduced-motion is set:

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
  document.addEventListener('mousemove', sparkleHandler);
}

4. Component Specifications Audit

Focus States

The design system defines: - --ul-focus-ring: 0 0 0 2px var(--ul-bg), 0 0 0 4px var(--ul-accent-functional) - --ul-focus-ring-inset: inset 0 0 0 2px var(--ul-accent-functional)

Assessment: The 2px gap + 2px ring pattern is excellent — provides clear visibility on both light and dark backgrounds, with the gap ensuring the ring is visible against the element itself. Uses opal-600 (#0d9488) which passes contrast. PASS.

All 18 component specs include focus state definitions. PASS.

Keyboard Navigation

Component Keyboard Pattern ARIA Status
Button Enter/Space to activate implicit PASS
Card (interactive) Enter to activate needs role="link" or <a> Needs spec
Input standard form control implicit PASS
Toggle Space to toggle role="switch", aria-checked PASS
Modal Escape to close, focus trap role="dialog", aria-modal PASS
Tab Group Arrow keys between tabs role="tablist/tab/tabpanel" PASS
Dropdown Arrow keys, Enter, Escape role="menu/menuitem" PASS
Checkbox Space to toggle implicit PASS
Radio Arrow keys within group implicit PASS
Tooltip Shows on focus role="tooltip", aria-describedby PASS

Finding: One gap — interactive cards need explicit keyboard roles in the spec.

Touch Targets

WCAG 2.2 requires 24x24px minimum touch targets (AA).

Component Min Size Pass?
Button (small) 32px tall PASS
Button (default) 40px tall PASS
Toggle 44x24px PASS
Checkbox 18px box, but clickable label area extends PASS (with label)
Tab item ~40px tall (padding 8px + 16px text) PASS
Close button (modal) Depends on implementation — spec says icon-only Needs 24x24 minimum

Motion

All component specs reference motion tokens from --ul-duration-* and --ul-ease-*. The design system CSS includes:

@media (prefers-reduced-motion: reduce) {
  :root {
    --ul-duration-instant: 0ms;
    --ul-duration-fast: 0ms;
    --ul-duration-normal: 0ms;
    --ul-duration-slow: 0ms;
    --ul-duration-slower: 0ms;
  }
}

This zeros all durations when reduced motion is preferred. Components using these tokens automatically respect the preference. PASS.


5. Dark Mode Accessibility

Shadow Behavior

Current shadows use rgba(28, 25, 23, opacity) — warm black. On dark backgrounds, these shadows are invisible because they’re the same value as the background.

Recommendation: Add dark-mode-specific shadow tokens that use lighter, more diffuse shadows:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --ul-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
    --ul-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
    --ul-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2);
    --ul-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.3), 0 4px 6px rgba(0, 0, 0, 0.2);
    --ul-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4), 0 8px 10px rgba(0, 0, 0, 0.2);
  }
}

Alternatively, dark mode can rely more on border differentiation (--ul-border already switches to stone-700) and reduce shadow dependence.

Focus Ring in Dark Mode

The focus ring uses var(--ul-bg) as the gap color and var(--ul-accent-functional) as the ring. In dark mode, --ul-bg becomes stone-900 and opal-600 maintains 4.6:1 contrast against it. PASS.


6. Summary

Passes

Needs Fixing

Issue Priority Location
Homepage subtitle contrast 2.5:1 Critical Live site
Homepage no prefers-reduced-motion Serious Live site
Homepage no landmarks/skip nav Moderate Live site
Add --ul-warning-text token (#92400e) Minor Design system tokens
Dark mode shadows invisible Minor Design system CSS
Interactive card needs keyboard role in spec Minor Component spec doc
Modal close button needs 24x24 minimum Minor Component spec doc

No Token Changes Required

The existing color tokens are correctly structured. The semantic aliases (--ul-accent, --ul-accent-functional, --ul-accent-contrast) map to appropriate contrast levels. The documentation (do’s/don’ts) is the right place to enforce proper usage — not new tokens.

One Optional Token Addition

"warning-text": { "$value": "#92400e", "$type": "color", "$description": "Warning text on white — passes AA at 7.1:1" }

This gives a safe text color for warning contexts without requiring the tinted background workaround.