Skip to content

feat(react-ui): add scheme-pinned defaults-dark.css and defaults-light.css exports#737

Open
ankit-thesys wants to merge 1 commit into
mainfrom
ankit/th-2215-scheme-pinned-defaults-exports
Open

feat(react-ui): add scheme-pinned defaults-dark.css and defaults-light.css exports#737
ankit-thesys wants to merge 1 commit into
mainfrom
ankit/th-2215-scheme-pinned-defaults-exports

Conversation

@ankit-thesys

@ankit-thesys ankit-thesys commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #736

Problem

Every dark token the package ships sits behind @media (prefers-color-scheme: dark), in ./index.css, ./layered/styles/index.css, and ./defaults.css alike. An app that forces one scheme with <ThemeProvider mode="dark"> therefore renders the stock light theme on light scheme devices wherever the runtime style injection is not in effect, meaning the static first paint, server rendered HTML before hydration, hydration failures, and pages without JavaScript. A downstream dark only consumer hit this in production and now scrapes the dark token block out of the dist CSS on every upgrade as a workaround. Full investigation: docs/dark-scheme-exports-investigation.md.

Change

Two new scheme pinned stylesheets, emitted from the same generator that already produces openui-defaults.css so they come from the token source of truth and cannot drift:

  • @openuidev/react-ui/defaults-dark.css is a plain unlayered :root block with the full dark token set and no media query.
  • @openuidev/react-ui/defaults-light.css is the same for light, since light only apps have the mirrored problem on dark scheme devices.

A dark only app pins the scheme statically with one extra import:

@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@import "@openuidev/react-ui/layered/styles/index.css";
@import "@openuidev/react-ui/defaults-dark.css";
/* the consumer's own :root brand token overrides follow and win by source order */

Details

  • src/scripts/generate-css-utils.ts gains a generateSchemePinnedDefaults helper and emits openui-defaults-dark.scss and openui-defaults-light.scss next to the existing generated files. Both are committed, matching how openui-defaults.scss is tracked.
  • css-layer-utils.mjs exports a shared DEFAULTS_CSS_FILES list, and the layered mirror keeps all three defaults files unlayered, matching the existing contract for openui-defaults.css.
  • cp-css.js strips the BOM from and copies all three defaults files into dist/styles.
  • package.json adds the ./defaults-dark.css and ./defaults-light.css exports.
  • check-css-artifacts.js now asserts the pinned files stay unlayered in both trees, contain no media query, and keep token count parity with each other and with the combined openui-defaults.css, so a regression fails the build instead of shipping.
  • README gains a "Single-scheme apps" section and the new rows in the subpath exports table.

Verification

pnpm build passes including check:css (55 layered files verified), and pnpm test passes with a new test covering the verbatim mirror of the pinned files. The emitted artifacts each contain 213 tokens, matching the dark media block exactly, with no @media and no @layer, and require.resolve finds both new export paths from a sibling workspace package.

🤖 Generated with Claude Code

…t.css exports

Every dark token the package ships is gated behind
@media (prefers-color-scheme: dark), so an app forcing one scheme
(<ThemeProvider mode="dark">) renders the stock light theme on
light-scheme devices wherever the runtime style injection is not in
effect: static first paint, server-rendered HTML before hydration,
hydration failures, and no-JS. A downstream dark-only consumer hit this
in production and now scrapes the dark token block out of the dist CSS
on every upgrade.

Emit plain unlayered :root blocks for each scheme from the same
generator that produces openui-defaults.css, ship them as
./defaults-dark.css and ./defaults-light.css (unlayered in both style
trees, like ./defaults.css), and guard them in check:css: no media
query may sneak back in, and token counts must stay in parity with the
combined defaults file.

Fixes #736
Linear: TH-2215

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dark only apps render the stock light theme on light scheme devices because there is no scheme pinned CSS export

1 participant