You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug(ui): V2 AnimatedNumber exposes animation digits and reversed reading order to screen readers #54653
V2's AnimatedNumber still exposes animation cells to assistive technology and gives multi-digit numbers the wrong reading order.
With the unmodified production component and CSS rendered in Chromium / Solid 1.9.15:
Value 12 looks correct in LTR, but the accessibility snapshot contains text 2, then 1.
During a 5 to 6 update, the accessibility snapshot exposes 0123456789 three times instead of the actual count. The existing motion-duration CSS variable was increased only to capture this transient state.
The component reverses digits in DOM order and renders 30 cells per animated strip, without aria-hidden on the visual digits. The parent span's aria-label does not replace those children in the observed accessibility tree: production component.
CSS hides the strip at rest, but exposes it while animating; visual row-reverse does not repair accessibility reading order: production CSS. A current consumer is ToolCountLabel.
OpenCode version
2.0.26 source build, latest v2 commit 4617210822bdbb31e5749751afbfb4584ccedac8 (audited 2026-10-12). Both component and CSS are unchanged at current dev commit 707de5832656e89cc0f04436aa1cd5f4f91c5763.
Steps to reproduce
Render <AnimatedNumber value={12} /> with its production CSS, inspect its accessibility tree, then increment a single-digit value during animation.
Expected: one correctly ordered, current numeric value. Expose stable text for that value and hide the visual animation from assistive technology regardless of motion state.
Related reports
V2 continuation of Problem 2 in #41408, closed during V1 migration triage. #43643 concerns visual RTL ordering in the same component. Streaming-content exposure is tracked separately in #54652 and addressed by PR #51084, which does not fix this component.
Thanks for the detailed report. I was able to reproduce this. I rendered the production AnimatedNumber with its CSS in headless Chromium and read the Playwright accessibility snapshot:
value={12} is exposed as 2 1.
During a 5 → 6 update (with --tool-motion-odometer-ms raised so the snapshot lands mid-animation), it is exposed as 0 1 2 3 4 5 6 7 8 9 three times.
The component and CSS are the same in v2.0.26 and on the latest dev (707de58), so it still happens there. The script below reproduces it.
Related: #43643 (wrong visual digit order in RTL locales, caused by the same DOM reversal), #41408 (the v1 report of this problem).
Reproduction script (repro.ts)
// Reproduces #54653: AnimatedNumber's accessibility tree (reading order + animation cells).// Usage: REPO=/workspace/repo bun repro.ts (needs solid-js, esbuild, esbuild-plugin-solid, playwright + chromium)import{build}from"esbuild"import{solidPlugin}from"esbuild-plugin-solid"import{chromium}from"playwright"import{readFileSync,writeFileSync,mkdirSync}from"fs"constrepo=process.env.REPO??"/workspace/repo"constdir="/tmp/opencode/an"mkdirSync(dir,{recursive: true})writeFileSync(`${dir}/animated-number.tsx`,readFileSync(`${repo}/packages/ui/src/components/animated-number.tsx`))writeFileSync(`${dir}/entry.tsx`,`import { render } from "solid-js/web"import { createSignal } from "solid-js"import { AnimatedNumber } from "./animated-number"const [a] = createSignal(12)const [b, setB] = createSignal(5);(window as any).bump = () => setB(6)render(() => <div><p id="a"><AnimatedNumber value={a()} /></p><p id="b"><AnimatedNumber value={b()} /></p></div>, document.getElementById("root")!)`)constout=awaitbuild({entryPoints: [`${dir}/entry.tsx`],bundle: true,write: false,format: "esm",nodePaths: [`${import.meta.dir}/node_modules`],plugins: [solidPlugin()],conditions: ["browser"]})constcss=readFileSync(`${repo}/packages/ui/src/components/animated-number.css`,"utf8")consthtml=`<!doctype html><html><head><style>${css} :root{--tool-motion-odometer-ms:20000ms}</style></head><body><div id="root"></div><script type="module">${out.outputFiles[0].text}</script></body></html>`constbrowser=awaitchromium.launch()constpage=awaitbrowser.newPage()awaitpage.setContent(html)awaitpage.waitForSelector("#b [data-component=animated-number]")console.log("== value 12, at rest ==\n"+awaitpage.locator("#a").ariaSnapshot())console.log("innerText:",JSON.stringify(awaitpage.locator("#a").innerText()))console.log("== value 5, at rest ==\n"+awaitpage.locator("#b").ariaSnapshot())awaitpage.evaluate(()=>(windowasany).bump())awaitpage.waitForTimeout(300)console.log("== 5 -> 6, mid-animation ==\n"+awaitpage.locator("#b").ariaSnapshot())awaitpage.screenshot({path: "/workspace/repro/animating.png"})awaitbrowser.close()
I read the component at packages/ui/src/components/animated-number.tsx and the report matches the code: digits() reverses the label (digits = [...label].reverse()), so the DOM exposes 2 before 1 for 12, and each Digit renders a 30-cell TRACK strip that stays in the accessibility tree whether or not it is animating — data-animating is only a visual hook and aria-label on the root does not replace its children.
My plan, kept to the a11y contract the issue names:
hide the decorative layer from assistive technology — the digit strip and the reversed static digit both live under one wrapper, so a single aria-hidden="true" covers the whole visual tree
expose the current value once, in reading order, through the accessible name on the root, so a value is announced as 12 and not as 2 1
No visual change: the strip stays in the DOM and the CSS keeps driving the odometer, and the reversed DOM order plus row-reverse stays as it is — the fix is about what assistive technology sees, not about what the eye sees.
I will verify with a component test in the same Storybook harness the rest of the repo's browser tests use (packages/ui/src/**/*.stories.tsx is already picked up by the shared Storybook config): it renders the production component and asserts the accessibility snapshot is the current value in order at rest and mid-animation, and that no animated-number-cell is exposed.
Opening the PR against dev as soon as today's output cap allows.
Activity
opencode-agent commentedon Oct 12, 2026
Thanks for the detailed report. I was able to reproduce this. I rendered the production
AnimatedNumberwith its CSS in headless Chromium and read the Playwright accessibility snapshot:value={12}is exposed as2 1.5→6update (with--tool-motion-odometer-msraised so the snapshot lands mid-animation), it is exposed as0 1 2 3 4 5 6 7 8 9three times.The component and CSS are the same in v2.0.26 and on the latest
dev(707de58), so it still happens there. The script below reproduces it.Related: #43643 (wrong visual digit order in RTL locales, caused by the same DOM reversal), #41408 (the v1 report of this problem).
Reproduction script (repro.ts)
Run it:
bun repro.tsargszero commentedon Oct 12, 2026
I'd like to work on this.
I read the component at
packages/ui/src/components/animated-number.tsxand the report matches the code:digits()reverses the label (digits = [...label].reverse()), so the DOM exposes2before1for12, and eachDigitrenders a 30-cellTRACKstrip that stays in the accessibility tree whether or not it is animating —data-animatingis only a visual hook andaria-labelon the root does not replace its children.My plan, kept to the a11y contract the issue names:
aria-hidden="true"covers the whole visual tree12and not as2 1No visual change: the strip stays in the DOM and the CSS keeps driving the odometer, and the reversed DOM order plus
row-reversestays as it is — the fix is about what assistive technology sees, not about what the eye sees.I will verify with a component test in the same Storybook harness the rest of the repo's browser tests use (
packages/ui/src/**/*.stories.tsxis already picked up by the shared Storybook config): it renders the production component and asserts the accessibility snapshot is the current value in order at rest and mid-animation, and that noanimated-number-cellis exposed.Opening the PR against
devas soon as today's output cap allows.fix(ui): read AnimatedNumber once, in order, in any direction
fix(ui): read AnimatedNumber once, in order, in any direction
fix(ui): read AnimatedNumber once, in order, in any direction