Sandworm

Accessibility

ready

Contrast ratios, focus states, gradient text-fill vendor-prefix requirements, and reduced-motion rules for the Sandworm design system. Ratios computed 2026-06-10 via WCAG 2.1 relative-luminance math.

Contrast ratios — dark surfaces

Key foreground/background pairs on stone-950 /stone-975. The critical caveat: magenta-600 on dark is 3.1:1 — display/headline scope only. For body-size magenta on dark, use magenta-400 (5.6:1).

stone-025 on stone-975
18.8:1AAA
stone-300 on stone-950
7.8:1AAA
stone-500 on stone-950
4.1:1AA large only

Large text (18pt+/14pt bold) or non-essential metadata only — fails AA at body size

sand-300 on stone-950
10.7:1AAA
magenta-600 on stone-950
3.1:1AA large only

Display/headline only (≥24px). Fails AA at body size. Use magenta-400 for small text on dark.

magenta-400 on stone-950
5.6:1AA

Use this for body-size magenta accents on dark surfaces

violet-600 on stone-950
3:1AA large only

The warm-hero gradient's violet tail. Same large-text scope as magenta-600 on dark.

Contrast ratios — light surfaces

Key pairs on stone-025. Note that magenta-600 is comfortably AA on light (5.8:1) — the dark-mode caveat does not apply here.

stone-900 on stone-025
16.6:1AAA
stone-700 on stone-025
8.2:1AAA
magenta-600 on stone-025
5.8:1AA

Comfortably AA on light — the dark-mode caveat does not apply here

sand-700 on stone-025
5.8:1AA

Brand emphasis and the dark-mode caveat

The signature emphasis devices — font-semibold text-magenta-600 spans and the warm-hero gradient text-fill — are display patterns, not body-text patterns, on dark surfaces.

  • Hero/headline emphasis (h1/h2, ≥24px) — magenta-600 and gradient text-fills are fine on dark
  • Body-size emphasis on light surfaces — magenta-600 is 5.8:1, comfortably AA
  • Body-size magenta accent on dark — use magenta-400 (5.6:1)
  • Don't carry essential meaning in body-size magenta-600 or gradient text on dark — at those sizes it's decorative emphasis; the surrounding font-light copy must carry the content

Gradient text-fill profile on stone-950: sand-300 end 10.7 → red-400 mid 7.6 → violet-600 tail 3.0. The tail is the constraint — gradient emphasis carries the same large-text scoping as solid magenta.

Gradient text-fill — Safari vendor prefix rule

background-clip: text requires both the standard and -webkit- forms or Safari/iOS renders the gradient as a background block instead of clipping to the text shape. Always include all three declarations.

Required CSS pattern

background: linear-gradient(to right, #ffb370, #f9808a, #6242e0);
background-clip: text;
-webkit-background-clip: text;       /* Safari */
-webkit-text-fill-color: transparent; /* Safari */
color: transparent;                    /* fallback */

Without -webkit-text-fill-color: transparent, Safari may render the original text color over the clipped gradient.

Live example — warm-hero gradient headline

Sandworm.

sand-300 → red-400 → violet-600 · warm-hero gradient

Backdrop blur — also needs -webkit-

backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);   /* Safari */
background-color: rgba(23, 13, 28, 0.9);  /* stone-950/90 fallback */

Card-dark and modal surfaces. Always include a solid background fallback — backdrop-filter fails silently in browsers that don't support it.

Focus rings

Every interactive element needs a visible focus ring on keyboard navigation. Use :focus-visible (not :focus) so the ring only appears when needed — keyboard nav, not mouse click. Keyboard accessibility is a brand requirement: if a custom interactive element doesn't have a visible focus ring, it isn't shippable.

Token spec

Ring color: magenta-600 at 2px

Ring offset: 2px

Offset color on dark: stone-950 (matches surface)

Offset color on light: stone-025 (matches surface)

Focusable demo — Tab to this element

Focusable link

Tailwind utility pattern

/* Applied to interactive elements */
focus-visible:ring-2
focus-visible:ring-[hsl(var(--magenta-600))]
focus-visible:ring-offset-2
focus-visible:ring-offset-[hsl(var(--stone-950))]  /* dark */
focus-visible:ring-offset-[hsl(var(--stone-025))]  /* light */

Don't substitute the browser's default blue focus ring — the magenta-600 ring works on both modes because magenta-600 is a brand spine color, not a status color.

Disabled state

opacity: 0.5

pointer-events: none

Reduced motion

@media (prefers-reduced-motion: reduce) is non-optional for any animated surface. Ship the reduced-motion off-switch in the same change as the animation. If an animation can't degrade gracefully, gate it explicitly.

Recommended global baseline

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

This is the recommended hub pattern. The marketing site currently implements reduced motion per-surface rather than via one global wildcard — both approaches are valid.

Per-surface pattern (current marketing site)

  • Rotating border — drops to animation: none + static linear-gradient fallback
  • Marquee logo wall — animation: none !important
  • Float/twinkle/spinner loops — animation: none
  • JS-driven loops — read window.matchMedia('(prefers-reduced-motion: reduce)') and halt requestAnimationFrame loops

JS guard pattern for animated components

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (!prefersReducedMotion) {
  requestAnimationFrame(animationLoop);
}

Touch targets

Interactive touch targets should be at minimum 44×44px (WCAG 2.5.5 AAA target; the AA bar is 24×24px). For icon-only buttons, use a wrapper with padding to meet the minimum hit area even if the visual element is smaller. The size="icon" Button variant defaults to h-9 w-9 (36px) — add h-11 w-11 for primary touch surfaces.