feat(react-ui): adopt CSS cascade layers (@layer openui) via build-side post-process#589
Open
ankit-thesys wants to merge 6 commits into
Open
Conversation
CSS @layer rules require Chrome 99+ / Firefox 97+ / Safari 15.4+ / Edge 99+. Declaring the floor before the cascade-layer wrap lands so consumers and tooling can detect incompatible target browsers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a Styling Integration section to the package README covering the cascade-layer contract: consumer CSS overrides OpenUI components without specificity matching, with a Tailwind v4 layer-order example, a note that Tailwind v3 / CSS Modules / CSS-in-JS work zero-config, and the browser support floor declared via browserslist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… unlayered Add a two-line comment above the useInsertionEffect that writes <style> into <head>. The runtime injection is unlayered by design so it overrides every @layer openui declaration (the static defaults file and the component CSS). Without this annotation a future refactor could silently move the injection into a cascade layer and break the override contract documented in the README. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add @layer theme, base, openui, components, utilities; ahead of @import "tailwindcss"; in every example app's globals.css. All nine example apps use Tailwind v4; without this declaration the cascade order is bundler-dependent and OpenUI typically ends up declared after @layer utilities, preventing utility classes from overriding component styles. With the declaration the order is theme < base < openui < components < utilities — Preflight resets the baseline, OpenUI applies on top, Tailwind utilities and consumer overrides win above OpenUI. Affected examples: multi-agent-chat, mastra-chat, hands-on-table-chat, openui-chat, openui-artifact-demo, openui-dashboard, shadcn-chat, supabase-chat, vercel-ai-chat. Verified in openui-chat at runtime: a rule injected into @layer utilities now correctly overrides .openui-button-base-primary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document the cascade-layer integration across the four most relevant docs pages so consumers see the recipe at every plausible entry point: - chat/installation.mdx — new Step 3 walks Tailwind v4 users through declaring @layer theme, base, openui, components, utilities; ahead of @import tailwindcss. Explains why bundler-determined order is fragile and notes that non-Tailwind-v4 setups need no configuration. - chat/theming.mdx — new "Override component styles with CSS" section shows the unlayered-CSS override pattern and links to the v4 recipe. - api-reference/react-ui.mdx — new "Cascade-layer contract" subsection under Import gives the one-paragraph summary plus links to the chat docs for setup and override patterns. - openui-lang/standard-library.mdx — brief note in the render-with-OpenUI section pointing readers at the chat docs for the contract. Each addition includes the browser support floor (Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+; March 2022 baseline). Anchor links across the four pages cross-reference each other so any entry point reaches the full picture in one click. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ss.js Build-side post-process: after Sass emits dist/components/**/*.css, cp-css.js walks the tree and wraps each file in @layer openui { ... } before the existing copy step carries it to dist/styles/. Component SCSS sources stay unchanged. Why post-process instead of source-side @layer wraps: - One change site enforces the contract; no per-file discipline. - Source SCSS stays clean — no 105-file diff, no skill/rule template. - Storybook consumes source SCSS directly and behaves as before this change (unlayered → specificity-driven), so no preview-head.html or preflight layer wrap is needed. Skipped files: - *.module.css — Storybook CSS Modules, locally scoped, not shipped. - dist/openui-defaults.css — lives outside dist/components, stays unlayered so the defaults.css export remains in the unlayered cascade and ThemeProvider runtime injection keeps overriding it. The aggregate dist/components/index.css includes the :root { --openui-* } declarations from openui-defaults.scss (via Sass @use). Those land inside @layer openui in the aggregate; this is benign because var() lookups ignore layers and ThemeProvider's unlayered runtime injection plus the unlayered defaults.css export both still override at runtime. Idempotency check (/^\s*@layer\s+openui\b/) protects watch-mode and back-to-back builds from double-wrapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #588.
Summary
Adopts CSS cascade layers (
@layer openui) for OpenUI's compiled component CSS so consumers can override component styles without!important, selector mirroring, or specificity stacking. Implementation is build-side post-process incp-css.js— component SCSS sources are unchanged.dist/components/**/*.css) is wrapped in@layer openui { ... }after Sass emits it.defaults.css(the:root { --openui-* }token export) stays unlayered — ThemeProvider runtime injection contract is unchanged.globals.css.What changes for consumers
OpenUI components now lose to:
globals.css:Without that declaration, the cascade order is bundler-determined and
openuitypically ends up declared afterutilities— utility overrides silently fail (see #588 Scenario A).ThemeProvidertoken customization,var(--openui-*)resolution, and thedefaults.csstoken-only export are unchanged.Browser support
CSS cascade layers require Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+ (March 2022 baseline). Declared via
browserslistinpackages/react-ui/package.jsonso consumer build tools see the floor explicitly.Why post-process instead of source-side
@layerwrapscp-css.js) enforces the contract — no per-file discipline.preview-head.html, no preflight layer wrap.Trade-off: in Storybook, OpenUI components win on specificity vs consumer overrides; in prod, consumers win via layer order. For "does this component look right" Storybook use, identical. For "does my consumer override beat OpenUI" QA, test in a consumer app.
We prototyped the source-side approach in parallel (105 SCSS source wraps + skill template + path-scoped rule + CI guard + Storybook preflight fix). Both produce identical consumer contracts. Post-process ships ~50× smaller diff (+112/-1 vs +7434/-7150) and avoids the maintenance overhead.
Test plan
pnpm install && pnpm -r build.pnpm --filter @openuidev/react-ui storybook— confirm components render correctly. (Storybook needs no changes for this approach.)cd examples/openui-chat && pnpm dev— confirm components look right in a real consumer app..openui-button-base-primary { background: hotpink }, or layered@layer utilities { .openui-button-base-primary { background: red } }) and confirm it wins without!important.pnpm --filter @openuidev/react-ui lint:checkandformat:checkare clean.head -c 50 packages/react-ui/dist/components/Button/button.cssshould start with@layer openui{.head -c 50 packages/react-ui/dist/openui-defaults.cssshould start with:root{(unlayered, intentional).Commits
0efae7b4— declare browserslist floor for cascade-layers supportf14358e8— document @layer openui contract in readme93707abd— note ThemeProvider runtime injection is intentionally unlayered71169060— adopt cascade-layer recipe for tailwind v4 example apps4a25d7f8— document @layer integration recipe in chat docsb8e70bae— wrap compiled component CSS in @layer openui via cp-css.js🤖 Generated with Claude Code