Repository navigation
Add a "Reduce motion" accessibility setting to Appearance #50
Description
Problem or Motivation
OpenWork animates heavily — the transcript, session list, dropdowns, toasts, the
pet, and the empty-composer entrance all use motion/react (framer-motion)
motion / AnimatePresence transitions. There is currently no way to turn
these down. Users who are sensitive to motion (vestibular disorders,
photosensitivity) or who simply prefer a calmer, snappier UI have no control:
the app never consults the OS "reduce motion" preference and offers no in-app
toggle.
Comparable desktop apps ship exactly this. Claude's desktop app, macOS, and
Windows all expose a Reduce motion / reduce animations accessibility control,
and the web platform standardizes it as prefers-reduced-motion. OpenWork's
Appearance settings already cover theme mode, color theme, font, language, pet,
and "rich tool descriptions" — an accessibility motion control is the conspicuous
gap.
Proposed Solution
Add a "Reduce motion" toggle to Settings → Appearance → Interface (next to
"Rich tool descriptions"). When enabled it:
- Forces
motion/reactto short-circuit allmotioncomponent animations
app-wide by rendering the app inside<MotionConfig reducedMotion="always">
(when off,reducedMotion="user"so the OSprefers-reduced-motionsetting is
still respected — a strictly better default than today's "always animate"). - Sets
data-reduce-motion="true"on<html>, and a global CSS rule collapses
CSS animations/transitions (animation-duration/transition-duration→
~0ms) so non-motionCSS effects are covered too. - Persists the choice in the renderer's
localStorage(the same
lib/local-storage.tsmechanism used by other renderer UI prefs like
showConnectionIcons), so it survives restarts.
Feasibility
Classification: pure frontend — candidate. No qwen-code backend involvement.
motionis already a dependency (apps/electron+packages/ui);MotionConfig
propagates through React context to everymotioncomponent in the tree,
including those inpackages/ui, so wrapping once at the renderer root
(main.tsx'sRoot) covers the whole app.- Persistence reuses the existing
lib/local-storage.tsKEYS/get/sethelper
— no new IPC, noconfig.json, no main-process change beyond the renderer. - The toggle reuses the existing
SettingsTogglecomponent and Appearance page
structure. Reusessettings.appearance.interfacesection heading; two new i18n
keys (settings.appearance.reduceMotion/...Desc) added to all 7 locales.
Alternatives Considered
- Respect only the OS
prefers-reduced-motion(no in-app toggle): better than
nothing, but users often want reduced motion in one app without changing an
OS-wide setting. The proposed default already honors the OS value and adds an
explicit override on top. - A per-animation opt-out: far more invasive and easy to miss spots; a single
root-levelMotionConfig+ CSS guard is the standard, comprehensive approach.
Additional Context
Acceptance criteria (CDP e2e assertion)
A new e2e/assertions/reduce-motion.assert.ts drives the real built app over CDP
entirely in the draft/no-session state (no seeded conversation, no backend
connection needed — it only touches Settings):
- Open Settings → Appearance from the sidebar; the Reduce motion toggle
renders and is initially off (aria-checked="false"), and<html>has no
data-reduce-motionattribute. - Click the toggle → it flips to
aria-checked="true",<html>gains
data-reduce-motion="true", and thelocalStoragekey
(craft-reduce-motion) is persisted astrue— proving the toggle actually
applies and persists the preference, not merely renders. - Click again → the toggle returns to
aria-checked="false", the<html>
attribute is removed, and the persisted value isfalse.
Part of the autonomous desktop-feature loop (loop-bot).
Activity