Skip to content

data-storyloaded attribute is set before story is visually rendered #629

@anilanar

Description

@anilanar

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

  • @ladle/react v5.1.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions