Summary
The documented [data-storyloaded] selector fires before the story component is actually rendered, causing visual snapshot tools (Playwright, Cypress, etc.) that follow the recommended pattern to capture blank/white screenshots.
Root cause
data-storyloaded is set in two places, and the first one fires too early:
1. Vite plugin injection (lib/cli/vite-plugin/vite-plugin.js)
Every .stories. file gets this appended during module transformation:
document.documentElement.setAttribute("data-storyloaded", "");
This runs at module parse time, before any React rendering happens. It also gets baked into production builds, so it affects static serving too.
2. Ring component lifecycle (lib/app/src/icons.tsx)
export const Ring = () => {
React.useEffect(() => {
document.documentElement.removeAttribute("data-storyloaded");
return () => document.documentElement.setAttribute("data-storyloaded", "");
}, []);
// ...
};
The Ring (Suspense fallback spinner) removes the attribute on mount and re-adds it on unmount. This is the correct timing — it fires when Suspense resolves and the story is rendered.
The problem: page.waitForSelector("[data-storyloaded]") resolves on the early setAttribute from the Vite plugin, before the Ring even mounts to remove it.
Reproduction
Use the documented Playwright pattern from the visual snapshots guide:
await page.goto(`${url}/?story=${storyKey}&mode=preview`);
await page.waitForSelector("[data-storyloaded]");
await expect(page).toHaveScreenshot(); // blank/white screenshot
Suggested fix
Remove the Vite plugin injection of data-storyloaded. The Ring component's useEffect cleanup already sets the attribute at the correct time (when Suspense resolves and the story is visible).
Workaround
Wait for the Ring spinner to be removed from the DOM instead of relying on data-storyloaded:
// Playwright
await page.locator('.ladle-ring-wrapper').waitFor({ state: 'detached' });
// Cypress
cy.get('.ladle-ring-wrapper', { timeout: 15_000 }).should('not.exist');
Environment
Summary
The documented
[data-storyloaded]selector fires before the story component is actually rendered, causing visual snapshot tools (Playwright, Cypress, etc.) that follow the recommended pattern to capture blank/white screenshots.Root cause
data-storyloadedis set in two places, and the first one fires too early:1. Vite plugin injection (
lib/cli/vite-plugin/vite-plugin.js)Every
.stories.file gets this appended during module transformation:This runs at module parse time, before any React rendering happens. It also gets baked into production builds, so it affects static serving too.
2. Ring component lifecycle (
lib/app/src/icons.tsx)The Ring (Suspense fallback spinner) removes the attribute on mount and re-adds it on unmount. This is the correct timing — it fires when Suspense resolves and the story is rendered.
The problem:
page.waitForSelector("[data-storyloaded]")resolves on the early setAttribute from the Vite plugin, before the Ring even mounts to remove it.Reproduction
Use the documented Playwright pattern from the visual snapshots guide:
Suggested fix
Remove the Vite plugin injection of
data-storyloaded. The Ring component'suseEffectcleanup already sets the attribute at the correct time (when Suspense resolves and the story is visible).Workaround
Wait for the Ring spinner to be removed from the DOM instead of relying on
data-storyloaded:Environment
@ladle/reactv5.1.1