Skip to content

Thinking timer ignores reduced-motion; send button lacks an accessible name #5588

Description

@BrunooMoniz

Prerequisites

  • I searched open issues and discussions and did not find an existing report of this bug.
  • This is not a security vulnerability. (Vulnerabilities go to GitHub Security Advisories — see SECURITY.md.)
  • I am running the latest code from the dev branch (the default branch you get on clone, where fixes land first) and the bug still reproduces there. Please git pull the latest dev before filing.

Install Method

Docker (docker compose up)

Operating System

Linux

Steps to Reproduce

Two small, related accessibility problems in the chat UI (static/js/chat.js). Both are client-side and reproduce with any model/backend.

A. The live "Thinking…" timer ignores prefers-reduced-motion:

  1. docker compose up -d --build and open the chat in a browser.
  2. Turn on reduced motion. Either at the OS level (for example macOS: System Settings > Accessibility > Display > Reduce motion) or in DevTools: open the Command Menu, run "Show Rendering", and set "Emulate CSS media feature prefers-reduced-motion" to reduce.
  3. Send a prompt to a model that streams a thinking/reasoning block, and watch the "Thinking…" header while it streams.

B. The send/stop button has no accessible name:

  1. Open the chat with a screen reader running (VoiceOver on macOS/iOS, or NVDA on Windows), or open DevTools > Elements > Accessibility pane.
  2. Focus the round send button next to the composer (the icon-only button that becomes a stop button while a response streams).
  3. Inspect its computed accessible name, both when idle and while a response is streaming.

Expected Behaviour

  • A. With reduced motion enabled, the thinking timer should not animate. It should update at a low, whole-second cadence (or not visibly tick) rather than repainting several times per second, honouring the user's motion preference like the rest of the UI.
  • B. The send/stop button should expose an accessible name that reflects its current action (for example "Send message" when idle and "Stop generation" while a response streams), so a screen-reader user can tell the two states apart.

Actual Behaviour

  • A. The timer animates regardless of the motion preference. It is driven by a self-perpetuating requestAnimationFrame loop that rewrites the header text roughly 60 times a second (a tenths-of-a-second readout) for the entire thinking duration, so a user who asked for reduced motion still gets a rapidly repainting counter.
  • B. The button has no aria-label. Its state is only carried in the title attribute, and title alone is not reliably announced by iOS VoiceOver on a <button>. As a result the accessible name is effectively empty (or an unlabelled "button"), and a screen-reader user cannot tell whether the control will send or stop, or notice that it switched to "Stop generation" mid-stream.

Logs / Screenshots

No screenshots — both problems are non-visual (one is behavioural under reduced motion, the other is screen-reader-only). The fixing PR includes a node-driven test that reproduces both contracts, and the PR description lists manual steps to verify with reduced motion emulated and with the DevTools accessibility tree.

Model / Backend (if relevant)

Not relevant — client-side.

Are you willing to submit a fix?

Yes — I can open a PR.

Additional Information

Both live in static/js/chat.js. For (A) the timer is set up where the live thinking header is created (_tickThinkTimer / _thinkTimerRAF); the fix drives it from a matchMedia-gated setInterval instead of requestAnimationFrame. For (B) the button already keeps its title in sync per mode, so the fix mirrors that live title into aria-label (which screen readers do announce) from the same single source of truth. PR to follow, targeting dev.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready for reviewDescription complete — ready for maintainer review

    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