Skip to content

feat(react-ui): adopt CSS cascade layers via build-side post-process#2

Closed
ankit-thesys wants to merge 6 commits into
mainfrom
feat/css-cascade-layers-postprocess
Closed

feat(react-ui): adopt CSS cascade layers via build-side post-process#2
ankit-thesys wants to merge 6 commits into
mainfrom
feat/css-cascade-layers-postprocess

Conversation

@ankit-thesys
Copy link
Copy Markdown
Owner

Summary

  • OpenUI's compiled component CSS is now wrapped in @layer openui so consumer CSS overrides components without specificity matching or !important.
  • Implementation lives entirely in cp-css.js: after Sass emits per-component and aggregate CSS, the build walks dist/components/ and wraps each file in @layer openui { ... }. Component SCSS sources are untouched.
  • All 9 example apps include the recommended Tailwind v4 layer-order declaration in their globals.css.
  • README + chat docs + API reference + standard library docs document the contract.

What changes for consumers

OpenUI components now lose to:

  • Unlayered consumer CSS (plain CSS, CSS Modules, CSS-in-JS, Tailwind v3 utilities) — zero configuration needed.
  • Tailwind v4 utilities, with this one-line addition to globals.css:
    @layer theme, base, openui, components, utilities;
    @import "tailwindcss";

ThemeProvider token customization, var(--openui-*) resolution, and the defaults.css token-only export are unchanged.

Browser support

Cascade layers require Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+ (March 2022 baseline). Declared via browserslist.

Why post-process instead of source-side @layer wraps

  • One change site (cp-css.js) enforces the contract — no per-file discipline.
  • Source SCSS stays clean — no 105-file diff, no skill/rule template, no separate CI guard.
  • 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.

The single 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 by default" Storybook use, it doesn't matter. For "does my consumer override beat OpenUI" QA, test in a consumer app.

Test plan

  • Pull the branch and run pnpm install && pnpm -r build
  • Run pnpm --filter @openuidev/react-ui storybook and confirm components render correctly
  • cd examples/openui-chat && pnpm dev — confirm components look right in a real consumer app
  • In the example, write a quick CSS override (e.g. .openui-button-base-primary { background: hotpink } unlayered, or @layer utilities { .openui-button-base-primary { background: red } }) and confirm it wins without !important
  • Confirm pnpm --filter @openuidev/react-ui lint:check and format:check are clean

🤖 Generated with Claude Code

ankit-thesys and others added 6 commits May 29, 2026 14:18
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>
@ankit-thesys
Copy link
Copy Markdown
Owner Author

Superseded by upstream PR thesysdev#589 (same branch, same commits). Closing here to avoid drift; review and merge happen on the upstream PR. Issue: thesysdev#588.

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.

1 participant