Light (default)
diff --git a/docs/content/docs/openui-lang/standard-library.mdx b/docs/content/docs/openui-lang/standard-library.mdx
index f8eeeaeef..3bebcde91 100644
--- a/docs/content/docs/openui-lang/standard-library.mdx
+++ b/docs/content/docs/openui-lang/standard-library.mdx
@@ -21,6 +21,8 @@ import { openuiLibrary } from "@openuidev/react-ui";
;
```
+The compiled stylesheet wraps component rules in `@layer openui` so your own CSS overrides them without specificity matching. See [Chat → Theming](/docs/chat/theming#override-component-styles-with-css) for the override contract, or [Chat → Installation](/docs/chat/installation#3-for-tailwind-v4-set-the-cascade-layer-order) for the Tailwind v4 setup.
+
## Generate prompt
Use the CLI to generate the system prompt at build time:
diff --git a/examples/hands-on-table-chat/src/app/globals.css b/examples/hands-on-table-chat/src/app/globals.css
index cec7e6528..e5922b2c3 100644
--- a/examples/hands-on-table-chat/src/app/globals.css
+++ b/examples/hands-on-table-chat/src/app/globals.css
@@ -1,3 +1,4 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
:root {
diff --git a/examples/mastra-chat/src/app/globals.css b/examples/mastra-chat/src/app/globals.css
index f1d8c73cd..4a090eef4 100644
--- a/examples/mastra-chat/src/app/globals.css
+++ b/examples/mastra-chat/src/app/globals.css
@@ -1 +1,2 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
diff --git a/examples/multi-agent-chat/src/app/globals.css b/examples/multi-agent-chat/src/app/globals.css
index e4ac5af9b..c45470e58 100644
--- a/examples/multi-agent-chat/src/app/globals.css
+++ b/examples/multi-agent-chat/src/app/globals.css
@@ -1,2 +1,3 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@import "@openuidev/react-ui/components.css";
diff --git a/examples/openui-artifact-demo/src/app/globals.css b/examples/openui-artifact-demo/src/app/globals.css
index 3d552a61f..0ffe00eb2 100644
--- a/examples/openui-artifact-demo/src/app/globals.css
+++ b/examples/openui-artifact-demo/src/app/globals.css
@@ -1,2 +1,3 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
diff --git a/examples/openui-chat/src/app/globals.css b/examples/openui-chat/src/app/globals.css
index 3d552a61f..0ffe00eb2 100644
--- a/examples/openui-chat/src/app/globals.css
+++ b/examples/openui-chat/src/app/globals.css
@@ -1,2 +1,3 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
diff --git a/examples/openui-dashboard/src/app/globals.css b/examples/openui-dashboard/src/app/globals.css
index 975bcec94..00eb02317 100644
--- a/examples/openui-dashboard/src/app/globals.css
+++ b/examples/openui-dashboard/src/app/globals.css
@@ -1,3 +1,4 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@keyframes openui-loading-bar {
diff --git a/examples/shadcn-chat/src/app/globals.css b/examples/shadcn-chat/src/app/globals.css
index d63189515..39a6d2c91 100644
--- a/examples/shadcn-chat/src/app/globals.css
+++ b/examples/shadcn-chat/src/app/globals.css
@@ -1,3 +1,4 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@custom-variant dark (&:is([data-theme="dark"] *));
diff --git a/examples/supabase-chat/src/app/globals.css b/examples/supabase-chat/src/app/globals.css
index f1d8c73cd..4a090eef4 100644
--- a/examples/supabase-chat/src/app/globals.css
+++ b/examples/supabase-chat/src/app/globals.css
@@ -1 +1,2 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
diff --git a/examples/vercel-ai-chat/src/app/globals.css b/examples/vercel-ai-chat/src/app/globals.css
index e4ac5af9b..c45470e58 100644
--- a/examples/vercel-ai-chat/src/app/globals.css
+++ b/examples/vercel-ai-chat/src/app/globals.css
@@ -1,2 +1,3 @@
+@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@import "@openuidev/react-ui/components.css";
diff --git a/packages/react-ui/README.md b/packages/react-ui/README.md
index 460cfef42..2f3044661 100644
--- a/packages/react-ui/README.md
+++ b/packages/react-ui/README.md
@@ -139,6 +139,37 @@ function App() {
| `defaultDarkTheme` | Built-in dark theme |
| `swatchTokens` | Token palette for use in theme builders |
+## Styling integration
+
+OpenUI's component styles live inside a CSS cascade layer named `openui`. Any unlayered consumer CSS overrides OpenUI without `!important` or specificity matching:
+
+```css
+@import "@openuidev/react-ui/components.css";
+
+/* Wins, no specificity tricks needed */
+.openui-button-base-primary { background: hotpink; }
+```
+
+### With Tailwind v4
+
+Declare layer order at the top of your entry stylesheet so `openui` sits above Tailwind's reset but below `components` and `utilities`:
+
+```css
+@layer theme, base, openui, components, utilities;
+@import "@openuidev/react-ui/components.css";
+@import "tailwindcss";
+```
+
+This places Tailwind's Preflight (in `base`) below OpenUI components so its element resets don't override them, while keeping utilities (`bg-red-500`, etc.) winning over OpenUI styles.
+
+### With Tailwind v3, CSS Modules, or CSS-in-JS
+
+No configuration needed — these all emit unlayered CSS, which automatically beats anything in `@layer openui`.
+
+### Browser support
+
+CSS cascade layers require Chrome 99+, Firefox 97+, Safari 15.4+, or Edge 99+ (all baseline from March 2022). On older browsers, the `@layer { ... }` block is dropped entirely and components render unstyled. The package declares this floor via the `browserslist` field in its `package.json`.
+
## Components
All components are available as individual imports:
diff --git a/packages/react-ui/cp-css.js b/packages/react-ui/cp-css.js
index c805b4941..dc8f9b78a 100644
--- a/packages/react-ui/cp-css.js
+++ b/packages/react-ui/cp-css.js
@@ -1,7 +1,7 @@
import fs from "fs";
import { camelCase } from "lodash-es";
import path from "path";
-import {fileURLToPath} from "url"
+import { fileURLToPath } from "url";
const dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -12,6 +12,29 @@ function ensureDirectoryExists(dirPath) {
}
}
+// Wrap a CSS file's contents in @layer openui { ... } if not already wrapped.
+// Idempotency check protects watch-mode and back-to-back builds.
+function wrapInLayer(content) {
+ if (content.trim() === "") return content;
+ if (/^\s*@layer\s+openui\b/.test(content)) return content;
+ return `@layer openui{${content}}`;
+}
+
+// Walk dist/components and wrap every emitted .css file in @layer openui.
+// *.module.css are Storybook CSS Modules — locally scoped, not shipped, not wrapped.
+function wrapComponentCssInPlace(dir) {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ wrapComponentCssInPlace(full);
+ } else if (entry.name.endsWith(".css") && !entry.name.endsWith(".module.css")) {
+ const content = fs.readFileSync(full, "utf8");
+ const wrapped = wrapInLayer(content);
+ if (wrapped !== content) fs.writeFileSync(full, wrapped, "utf8");
+ }
+ }
+}
+
// Replace .scss imports with .css imports in compiled JS files
function fixScssImportsInJs(dir) {
const entries = fs.readdirSync(dir);
@@ -37,6 +60,12 @@ function copyCssFiles() {
const srcDir = path.join(dirname, "dist", "components");
const distDir = path.join(dirname, "dist", "styles");
+ // Wrap every emitted component CSS in @layer openui before copying.
+ // dist/openui-defaults.css lives outside dist/components and stays unwrapped
+ // so the defaults.css export remains in the unlayered cascade — matching the
+ // ThemeProvider runtime injection contract.
+ wrapComponentCssInPlace(srcDir);
+
// Ensure the dist/styles directory exists
ensureDirectoryExists(distDir);
diff --git a/packages/react-ui/package.json b/packages/react-ui/package.json
index d381a04fb..7014497d5 100644
--- a/packages/react-ui/package.json
+++ b/packages/react-ui/package.json
@@ -193,6 +193,7 @@
"bugs": {
"url": "https://github.com/thesysdev/openui/issues"
},
+ "browserslist": "defaults and supports css-cascade-layers",
"eslintConfig": {
"extends": [
"plugin:storybook/recommended"
diff --git a/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx b/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx
index 1949046ce..424fab01a 100644
--- a/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx
+++ b/packages/react-ui/src/components/ThemeProvider/ThemeProvider.tsx
@@ -236,6 +236,8 @@ export const ThemeProvider = ({
const useAutoScope = isNested && !hasExplicitSelector;
const styleSelector = useAutoScope ? `.${scopedClassName}` : effectiveCssSelector;
+ // Intentionally unlayered — must override @layer openui so runtime theme
+ // switching takes effect. See README "Styling integration" before changing.
useInsertionEffect(() => {
const style = document.createElement("style");
style.setAttribute("data-openui-theme", id);