Surfaces are visual craft, not content. Treat them as decoration: hide them from the accessibility tree, keep focusable UI outside the canvas, and always ship a considered reduced-motion composition.
prefers-reduced-motion
Materials wrap motion in a shell that reads (prefers-reduced-motion: reduce). When it matches, the live renderer is not mounted — a static gradient (or still) takes its place.
// Pattern used by MaterialShell
const reduced = usePrefersReducedMotion()
const showStatic = forceStatic || !mounted || reduced
return showStatic
? <div aria-hidden style={fallbackGradient} />
: children- A frozen frame of an animation is not enough. Fallbacks use the same token colors arranged as a deliberate still composition.
- Test with OS settings and DevTools emulation — both should trigger the static path.
- Do not reintroduce motion with CSS keyframes on the fallback layer.
Decorative ARIA
- Mark the material root
aria-hidden(orrole="presentation") when it does not convey meaning. - Never put interactive controls inside the WebGL/canvas tree. Keep CTAs, links, and form fields in a sibling layer with normal document order.
- Materials must not trap focus or steal pointer events from overlays — use
pointer-events-noneon the surface when content sits above it. - If you need an accessible name for a marketing demo, put it on the section heading — not on the canvas.
Contrast for layered text
Each material’s detail page includes contrast guidance for foreground text. Rules of thumb:
- Prefer light text on dark-tinted scrims (
bg-black/40or a tokenized overlay) rather than guessing against a busy mesh. - High-frequency dither and grain can reduce effective contrast — increase type weight or add a solid panel behind copy.
- Check both light and dark token sets; a pass in one theme is not a pass in the other.
Also see performance pause behavior (background tabs) and WebGL failures.