pmndrs on GitHub

pmndrs/zustand

Reviewed against Rams quality heuristics — accessibility, hierarchy, color, typography, spacing, components, motion, and UX.

12 files reviewed·May 13, 2026

Top fix

Add prefers-reduced-motion guard to stop looping animation harming vestibular users.

See the fix

Verdict

Accessibility is the consistent failure mode — icon buttons, canvas elements, and language labels all invisible to assistive tech. The looping animation with no reduced-motion guard is the one issue that actively harms vestibular users.

Files Rams reviewed

examples/demo/src/components/CodePreview.jsx

examples/demo/src/components/CopyButton.jsx

examples/demo/src/components/Fireflies.jsx

examples/demo/src/components/Scene.jsx

examples/demo/src/components/Details.jsx

examples/demo/src/components/SnippetLang.jsx

examples/starter/src/index.tsx

examples/demo/src/pmndrs.css

examples/demo/src/styles.css

examples/demo/src/App.jsx

examples/demo/src/main.jsx

examples/starter/src/index.css

93/100

Accessibility

1 critical2 serious
Accessibility·examples/demo/src/components/CopyButton.jsx:21Critical

Icon-only button has no accessible name for screen readers

The <button className="copy-button"> renders only an SVG copy icon when `isCopied` is false. There is no aria-label, no visually-hidden text, and no title element on the SVG. Screen readers announce this as an unlabeled button.

Why it matters

Screen reader users encounter a button with no name and have no way to know what it does — keyboard-only users are blocked from understanding the control's purpose.

Fix

Every icon-only button needs an aria-label that names the action.

<button className="copy-button" onClick={handleCopy} {...props}>
<button className="copy-button" onClick={handleCopy} aria-label={isCopied ? 'Copied' : 'Copy code'} {...props}>
Accessibility·examples/demo/src/components/CodePreview.jsx:22Serious

language="tsx" mismatches the selected lang state — screen readers get the wrong label

The `<Highlight>` component has `language="tsx"` hardcoded, but the `lang` state toggles between `'javascript'` and `'typescript'`. The rendered language attribute on the `<pre>` element will always announce "tsx" regardless of which code is displayed, misleading assistive technology and syntax highlighters that use the language value for token parsing.

Why it matters

Screen readers and tooling that consume the `language-*` class on `<pre>` will always see "tsx" — users who switch to JavaScript get a mismatched code context, and the language toggle becomes semantically invisible.

Fix

Derive the Highlight language prop from the active lang state so the rendered output matches what is displayed.

<Highlight code={code} language="tsx" theme={undefined}>
<Highlight code={code} language={lang === 'javascript' ? 'javascript' : 'typescript'} theme={undefined}>
Accessibility·examples/demo/src/components/Scene.jsx:183Serious

Canvas element has no ARIA role or label, making it invisible to assistive tech

The `<canvas>` element returned by the `Canvas` component has no `role`, `aria-label`, or descriptive text. Screen readers encounter a nameless, purpose-unknown canvas — the entire animated scene is invisible to them.

Why it matters

Users on assistive tech get no information about this visual. Adding a role and label is a one-line fix that surfaces at least a description of what the canvas contains.

Fix

Add `role="img"` and `aria-label` to the canvas element to give screen readers a meaningful description of the decorative scene.

<canvas
  ref={canvas}
  style={{
<canvas
  ref={canvas}
  role="img"
  aria-label="Animated parallax forest scene with fireflies"
  style={{
97/100

Motion

1 critical
Motion·examples/demo/src/components/Fireflies.jsx:12Critical

Looping animation ignores prefers-reduced-motion, affecting vestibular users

The `useFrame` callback in `Fatline` unconditionally decrements `dashOffset` every frame, driving a permanent infinite animation. There is no check for `prefers-reduced-motion` and no way to pause it.

Why it matters

Users with vestibular disorders can experience nausea from continuous motion. An infinite looping animation with no reduced-motion path fails WCAG 2.3.3 and excludes a real subset of users.

Fix

Read the user's motion preference and skip the animation update when reduced motion is requested.

useFrame(
  (state, delta) =>
    (material.current.uniforms.dashOffset.value -= delta / 100),
)
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
useFrame(
  (state, delta) => {
    if (!prefersReduced) {
      material.current.uniforms.dashOffset.value -= delta / 100
    }
  },
)

Want this on your repo?

Rams reviews your next PR automatically and posts inline fix suggestions.

Review my public repoFree
98/100

Typography

1 serious
Typography·examples/starter/src/index.tsx:25Serious

Counter digits shift layout on every increment without tabular-nums

The counter `<span className="text-3xl">{count}</span>` renders proportional digits. As `count` grows from single to double to triple digits, each character width differs, causing the span — and the button next to it — to shift horizontally on every click.

Why it matters

Layout shift on a counter is the canonical example of why `tabular-nums` exists. In a starter template this is a teaching moment: developers who copy this pattern will repeat the mistake in production dashboards and pricing displays.

Fix

Apply `tabular-nums` to any number that updates dynamically so digit-width changes don't cause layout shift.

<span className="text-3xl">{count}</span>
<span className="text-3xl tabular-nums">{count}</span>
98/100

UX

1 serious
UX·examples/demo/src/App.jsx:16Serious

Counter button missing explicit type, defaults to submit inside any form

The `<button onClick={inc}>one up</button>` has no `type` attribute. HTML buttons default to `type="submit"`, which would accidentally submit any ancestor form this component is ever placed inside.

Why it matters

If Counter is ever embedded in a form context, the button submits the form instead of incrementing — a silent, hard-to-debug regression with no visible warning.

Fix

Always set type="button" on action buttons that are not intended to submit a form.

<button onClick={inc}>one up</button>
<button type="button" onClick={inc} aria-label="Increment count">one up</button>

Hierarchy

✓ No issues found

Color

✓ No issues found

Spacing

✓ No issues found

Components

✓ No issues found

Working well

  • Placing <SnippetLang> and <CopyButton> inside a <div className='snippet-container'> inside the <pre> is a deliberate layout choice that keeps the overlay controls semantically within the code block — so when the <pre> scrolls, the overlay can be positioned relative to it correctly. This is the right structural instinct, even if the inline style for position: relative should be moved to a class.
  • The error boundary pattern — catching WebGL init failure via `onError` and swapping to `<FallbackScene>` — is exactly right. Rather than letting a canvas crash silently or show a broken element, users get a meaningful static fallback image with proper alt text ('Zustand Bear'). This is the correct graceful degradation approach for any GPU-dependent content.
  • Colocating the Zustand store definition with the component that owns it is the right call for a scoped, single-use state like a language toggle. It avoids prop-drilling from a parent and keeps the language/code coupling explicit — the getCode selector that derives from lang makes the relationship between state and content readable at a glance.
  • Using `useAspect` for both `scaleN` and `scaleW` to drive plane sizes is clean responsive design at the 3D layer — the scene recomputes its geometry proportions based on actual viewport dimensions rather than hardcoding pixel values, which means the parallax layers fill the screen correctly at any aspect ratio.

Score your own repo with Rams.

Free on public repos. Reviews run on every pull request and post inline fix suggestions.

Review my public repoFree