Skip to content

Add a "Reduce motion" accessibility setting to Appearance #50

Description

@DragonnZhang

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/react to short-circuit all motion component animations
    app-wide by rendering the app inside <MotionConfig reducedMotion="always">
    (when off, reducedMotion="user" so the OS prefers-reduced-motion setting 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-motion CSS effects are covered too.
  • Persists the choice in the renderer's localStorage (the same
    lib/local-storage.ts mechanism used by other renderer UI prefs like
    showConnectionIcons), so it survives restarts.

Feasibility

Classification: pure frontend — candidate. No qwen-code backend involvement.

  • motion is already a dependency (apps/electron + packages/ui); MotionConfig
    propagates through React context to every motion component in the tree,
    including those in packages/ui, so wrapping once at the renderer root
    (main.tsx's Root) covers the whole app.
  • Persistence reuses the existing lib/local-storage.ts KEYS/get/set helper
    — no new IPC, no config.json, no main-process change beyond the renderer.
  • The toggle reuses the existing SettingsToggle component and Appearance page
    structure. Reuses settings.appearance.interface section 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-level MotionConfig + 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):

  1. Open Settings → Appearance from the sidebar; the Reduce motion toggle
    renders and is initially off (aria-checked="false"), and <html> has no
    data-reduce-motion attribute.
  2. Click the toggle → it flips to aria-checked="true", <html> gains
    data-reduce-motion="true", and the localStorage key
    (craft-reduce-motion) is persisted as true — proving the toggle actually
    applies and persists the preference, not merely renders.
  3. Click again → the toggle returns to aria-checked="false", the <html>
    attribute is removed, and the persisted value is false.

Part of the autonomous desktop-feature loop (loop-bot).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions