diff --git a/docs/content/docs/api-reference/react-ui.mdx b/docs/content/docs/api-reference/react-ui.mdx index 86a960d7f..629c89f5d 100644 --- a/docs/content/docs/api-reference/react-ui.mdx +++ b/docs/content/docs/api-reference/react-ui.mdx @@ -11,6 +11,12 @@ Use this package for prebuilt chat UIs and default component library primitives. import { Copilot, FullScreen, BottomTray } from "@openuidev/react-ui"; ``` +### Cascade-layer contract + +`@openuidev/react-ui/components.css` wraps every component rule in `@layer openui`. Unlayered consumer CSS overrides OpenUI components without specificity matching or `!important`. See [Chat → Theming](/docs/chat/theming#override-component-styles-with-css) for override patterns, and [Chat → Installation](/docs/chat/installation#3-for-tailwind-v4-set-the-cascade-layer-order) for the Tailwind v4 layer-order setup. + +Browser support: Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+ (all baseline from March 2022). + ## Layout components These layouts are documented in Chat UI guides and are all wrapped with `ChatProvider`. diff --git a/docs/content/docs/chat/installation.mdx b/docs/content/docs/chat/installation.mdx index 0b49eaa82..1f52d2ee2 100644 --- a/docs/content/docs/chat/installation.mdx +++ b/docs/content/docs/chat/installation.mdx @@ -57,7 +57,23 @@ export default function RootLayout({ children }: { children: React.ReactNode }) These imports give you the default chat layout styling and theme tokens. -## 3. Render a layout to verify setup +## 3. (For Tailwind v4) Set the cascade-layer order + +OpenUI's component styles live in `@layer openui`. If your app uses Tailwind v4, declare the layer order in your global stylesheet so `openui` sits above Tailwind's reset (`base`) but below `components` and `utilities`: + +```css +/* app/globals.css */ +@layer theme, base, openui, components, utilities; +@import "tailwindcss"; +``` + +This places OpenUI above Tailwind's Preflight (so its element resets don't override component styles) while keeping Tailwind utilities like `bg-red-500` winning over OpenUI. Without this declaration, the cascade order is bundler-dependent and `openui` may end up declared *after* `utilities`, which prevents utility overrides from taking effect. + +Tailwind v3, plain CSS, CSS Modules, and CSS-in-JS need no configuration — their styles are unlayered and beat anything in `@layer openui` automatically. + +See [`@openuidev/react-ui`](/docs/api-reference/react-ui#cascade-layer-contract) for the full styling integration contract. + +## 4. Render a layout to verify setup Render one of the built-in layouts on a page to confirm the package is installed correctly. diff --git a/docs/content/docs/chat/theming.mdx b/docs/content/docs/chat/theming.mdx index 945c060e1..81218c053 100644 --- a/docs/content/docs/chat/theming.mdx +++ b/docs/content/docs/chat/theming.mdx @@ -54,6 +54,20 @@ import { FullScreen } from "@openuidev/react-ui"; `disableThemeProvider` only skips the wrapper. It does not remove any chat functionality. +## Override component styles with CSS + +OpenUI's component styles live in `@layer openui`. Any unlayered consumer CSS overrides them without `!important` or matching specificity: + +```css +.openui-button-base-primary { + background: hotpink; +} +``` + +For Tailwind v4 apps, declare `@layer theme, base, openui, components, utilities;` ahead of `@import "tailwindcss";` in your global stylesheet (see [Installation](/docs/chat/installation#3-for-tailwind-v4-set-the-cascade-layer-order)) so utility classes also override OpenUI styles. CSS Modules, CSS-in-JS, and Tailwind v3 utilities emit unlayered CSS and override OpenUI automatically with no configuration. + +Browser support: Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+ (all baseline from March 2022). +

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);