Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/content/docs/api-reference/react-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
18 changes: 17 additions & 1 deletion docs/content/docs/chat/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 14 additions & 0 deletions docs/content/docs/chat/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<div className="grid md:grid-cols-2 gap-6 my-6">
<div>
<p className="text-sm text-center font-medium mb-2">Light (default)</p>
Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/openui-lang/standard-library.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { openuiLibrary } from "@openuidev/react-ui";
<Renderer library={openuiLibrary} response={streamedText} isStreaming={isStreaming} />;
```

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:
Expand Down
1 change: 1 addition & 0 deletions examples/hands-on-table-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";

:root {
Expand Down
1 change: 1 addition & 0 deletions examples/mastra-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";
1 change: 1 addition & 0 deletions examples/multi-agent-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@import "@openuidev/react-ui/components.css";
1 change: 1 addition & 0 deletions examples/openui-artifact-demo/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";

1 change: 1 addition & 0 deletions examples/openui-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";

1 change: 1 addition & 0 deletions examples/openui-dashboard/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";

@keyframes openui-loading-bar {
Expand Down
1 change: 1 addition & 0 deletions examples/shadcn-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";

@custom-variant dark (&:is([data-theme="dark"] *));
Expand Down
1 change: 1 addition & 0 deletions examples/supabase-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";
1 change: 1 addition & 0 deletions examples/vercel-ai-chat/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@layer theme, base, openui, components, utilities;
@import "tailwindcss";
@import "@openuidev/react-ui/components.css";
31 changes: 31 additions & 0 deletions packages/react-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 30 additions & 1 deletion packages/react-ui/cp-css.js
Original file line number Diff line number Diff line change
@@ -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));

Expand All @@ -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);
Expand All @@ -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);

Expand Down
1 change: 1 addition & 0 deletions packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"bugs": {
"url": "https://github.com/thesysdev/openui/issues"
},
"browserslist": "defaults and supports css-cascade-layers",
"eslintConfig": {
"extends": [
"plugin:storybook/recommended"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading