Provide a general summary of the issue here
SelectionIndicator (via SharedElement) schedules React state updates using flushSync inside queueMicrotask and requestAnimationFrame callbacks.
These fire after React's act() boundary closes during test renders, triggering:
An update to ForwardRef(SharedElement) inside a test was not wrapped in act(...)
When using vitest-fail-on-console — a common CI-safety package that converts console.error into hard test failures
Every test that renders a <Tabs> component built following the React Aria Tabs docs
(i.e. a custom Tab with <SelectionIndicator /> inside) will fail, even though the component renders and behaves correctly.
🤔 Expected Behavior?
- When running Vitest tests (browser mode, Chromium via Playwright) that render a
<Tabs> component containing <SelectionIndicator>, no console.error should be emitted.
- State updates triggered by rendering the component should be compatible with React's testing
act() boundary.
😯 Current Behavior
Every test that renders <Tabs> with <SelectionIndicator> logs the following console.error:
An update to ForwardRef(SharedElement) inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state /
});
/ assert on the output */
With vitest-fail-on-console (a common CI-safety package) this turns into a
hard test failure even though the component renders and behaves correctly.
Root cause — SharedElementTransition.tsx
The SharedElement component schedules state updates via flushSync inside asynchronous callbacks that fire after React's act() window has closed:
// line 122 — new element entering
queueMicrotask(() => flushSync(() => setState('entering')));
requestAnimationFrame(() => setState('visible')); // line 125
// line 130–134 — element exiting
queueMicrotask(() => {
if (scope[name]) {
flushSync(() => setState('exiting')); // line 133
Promise.all(element.getAnimations().map(a => a.finished))
.then(() => setState('hidden')); // line 136
}
});
// queueMicrotask fires after act() finishes.
// The flushSync call inside it synchronously flushes a React state update, which React detects as an out-of-act() update and logs the warning.
💁 Possible Solution
No response
🔦 Context
No response
🖥️ Steps to Reproduce
Clone and run the minimal reproduction below (or copy the files manually):
git clone https://github.com/lyhoang-web-dev/react-aria-shared-element-repro
cd react-aria-shared-element-repro
pnpm install
pnpm exec playwright install chromium
pnpm test
Expected: tests pass with no console errors
Actual: test fails — vitest-fail-on-console converts the console.error into a hard failure
Minimal reproduction files
package.json
{
"name": "react-aria-shared-element-repro",
"private": true,
"scripts": {
"test": "vitest"
},
"dependencies": {
"react": "19.2.4",
"react-dom": "19.2.4",
"react-aria-components": "1.16.0"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@vitejs/plugin-react": "4.5.0",
"@vitest/browser": "4.0.18",
"@vitest/browser-playwright": "4.0.18",
"playwright": "1.58.0",
"vitest": "4.0.18",
"vitest-browser-react": "2.0.4",
"vitest-fail-on-console": "0.10.1"
}
}
vite.config.ts
import react from "@vitejs/plugin-react";
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
test: {
setupFiles: ["./src/setup-tests.ts"],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: "chromium" }],
},
},
});
src/setup-tests.ts
import failOnConsole from "vitest-fail-on-console";
failOnConsole();
src/tabs.test.tsx
import { render } from "vitest-browser-react";
import { describe, expect, test } from "vitest";
import {
SelectionIndicator,
Tab as RACTab,
type TabProps as RACTabProps,
TabList,
TabPanel,
TabPanels,
Tabs,
composeRenderProps,
} from "react-aria-components";
/**
* Custom Tab that includes SelectionIndicator, matching the React Aria docs:
* https://react-aria.adobe.com/Tabs
*
* This is what triggers SharedElement → flushSync inside queueMicrotask,
* causing the "not wrapped in act" warning.
*/
function Tab(props: RACTabProps) {
return (
<RACTab {...props}>
{composeRenderProps(props.children, (children) => (
<>
{children}
<SelectionIndicator />
</>
))}
</RACTab>
);
}
describe("Tabs", () => {
test("renders the selected tab panel", async () => {
// Tab renders SelectionIndicator → SharedElement, which schedules state
// updates via flushSync inside queueMicrotask/requestAnimationFrame.
// These fire outside React's act() boundary, producing:
//
// "An update to ForwardRef(SharedElement) inside a test was not wrapped in act(...)"
//
// vitest-fail-on-console converts this console.error into a hard failure.
const screen = await render(
<Tabs defaultSelectedKey="overview">
<TabList aria-label="Sections">
<Tab id="overview">Overview</Tab>
<Tab id="details">Details</Tab>
</TabList>
<TabPanels>
<TabPanel id="overview">Overview content</TabPanel>
<TabPanel id="details">Details content</TabPanel>
</TabPanels>
</Tabs>,
);
await expect
.element(screen.getByRole("tab", { name: "Overview" }))
.toBeInTheDocument();
});
});
index.html
<!doctype html>
<html>
<head><meta charset="UTF-8" /></head>
<body><div id="root"></div></body>
</html>
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"]
},
"include": ["src"]
}
Version
1.16.0
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
Windows 11 Pro (10.0.26200)
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
Provide a general summary of the issue here
SelectionIndicator(viaSharedElement) schedules React state updates usingflushSyncinsidequeueMicrotaskandrequestAnimationFramecallbacks.These fire after React's
act()boundary closes during test renders, triggering:When using
vitest-fail-on-console— a common CI-safety package that convertsconsole.errorinto hard test failuresEvery test that renders a
<Tabs>component built following the React Aria Tabs docs(i.e. a custom
Tabwith<SelectionIndicator />inside) will fail, even though the component renders and behaves correctly.🤔 Expected Behavior?
<Tabs>component containing<SelectionIndicator>, noconsole.errorshould be emitted.act()boundary.😯 Current Behavior
Every test that renders
<Tabs>with<SelectionIndicator>logs the followingconsole.error:With
vitest-fail-on-console(a common CI-safety package) this turns into ahard test failure even though the component renders and behaves correctly.
Root cause —
SharedElementTransition.tsxThe
SharedElementcomponent schedules state updates viaflushSyncinside asynchronous callbacks that fire after React'sact()window has closed:💁 Possible Solution
No response
🔦 Context
No response
🖥️ Steps to Reproduce
Clone and run the minimal reproduction below (or copy the files manually):
Expected: tests pass with no console errors
Actual: test fails —
vitest-fail-on-consoleconverts theconsole.errorinto a hard failureMinimal reproduction files
package.json{ "name": "react-aria-shared-element-repro", "private": true, "scripts": { "test": "vitest" }, "dependencies": { "react": "19.2.4", "react-dom": "19.2.4", "react-aria-components": "1.16.0" }, "devDependencies": { "@types/react": "^19.2.14", "@vitejs/plugin-react": "4.5.0", "@vitest/browser": "4.0.18", "@vitest/browser-playwright": "4.0.18", "playwright": "1.58.0", "vitest": "4.0.18", "vitest-browser-react": "2.0.4", "vitest-fail-on-console": "0.10.1" } }vite.config.tssrc/setup-tests.tssrc/tabs.test.tsxindex.htmltsconfig.json{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "lib": ["ESNext", "DOM", "DOM.Iterable"] }, "include": ["src"] }Version
1.16.0
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
Windows 11 Pro (10.0.26200)
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response