` cannot use `getStyles()` at build time: it must be a style object, CSS text, a class `css()` gives, or a function of the theme giving one, or an array or condition of them
+```
+
+A file with several problems reports them all at once, in source order.
+
+See [Build Errors](/docs/build-errors) for the build errors, grouped by API.
diff --git a/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx b/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx
index 0cb08360b..ffa68a944 100644
--- a/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx
+++ b/apps/landing/src/app/(detail)/docs/migration/styled-components/page.mdx
@@ -121,6 +121,13 @@ Emotion's `
` behaves the same way: the `styles` prop is e
| `ThemeProvider`, `useTheme`, `withTheme` | `@devup-ui/react/compat`, CSS-variable backed |
| `createGlobalStyle`, `Global` | extracted, render nothing |
| `ServerStyleSheet`, `StyleSheetManager` | inert — Devup UI already emits a real stylesheet, so there is nothing to collect |
+| `CacheProvider` | renders its children; the cache it configures has nothing to hold |
| `isStyledComponent` | always `false` |
-| `ClassNames`, `CacheProvider` | no equivalent; stays on its own package with a build warning |
+| `css` prop, `jsx`, JSX runtime pragmas | compiled to `className` and `style` |
+| `ClassNames` | replaced by what its child renders, each `css` / `cx` call compiled to classes |
+| Component selectors (`${Child}`) | a marker class on the selected component |
| `.attrs()`, `.withConfig()` | attrs merged over props; `withConfig` dropped |
+
+## Limitations
+
+Composition only the runtime sees — a `className` from props, an external class, a `styled()` base the build cannot read — follows CSS specificity and stylesheet order rather than the order you wrote. Styled components and classes defined in another module are not read as build-time values, stylis plugins are not applied, and test utilities and React Native targets are not supported. A component of your own that takes the `css` prop must pass on both `className` and `style`. See [Supported Syntax & Limitations](/docs/limitations) for the full list.
diff --git a/apps/landing/src/app/(detail)/docs/migration/vanilla-extract/page.mdx b/apps/landing/src/app/(detail)/docs/migration/vanilla-extract/page.mdx
index df42ee57e..23d720a21 100644
--- a/apps/landing/src/app/(detail)/docs/migration/vanilla-extract/page.mdx
+++ b/apps/landing/src/app/(detail)/docs/migration/vanilla-extract/page.mdx
@@ -15,15 +15,24 @@ Inside `.css.ts` / `.css.js` — the only place vanilla-extract allows its APIs
// theme.css.ts
import { createTheme, createThemeContract, style } from '@vanilla-extract/css'
-const vars = createThemeContract({ colors: { bg: null } })
-export const light = createTheme(vars, { colors: { bg: 'white' } })
+export const vars = createThemeContract({ colors: { bg: null }, space: null })
+export const light = createTheme(vars, {
+ colors: { bg: 'white' },
+ space: '8px',
+})
export const box = style({ background: vars.colors.bg })
+export const base = style({ padding: 8 })
```
Numbers keep vanilla-extract's meaning, here and in ordinary modules: `padding: 8` is `8px`, and unitless properties such as `lineHeight` or `zIndex` stay as written. As in vanilla-extract, a number for a time gets `px` too (`transitionDuration: 300` is `300px`, which browsers ignore), so write times with their unit: `transitionDuration: '300ms'`.
A stylesheet can import other stylesheets and ordinary modules — ES modules or CommonJS — resolved like the bundler resolves them (relative paths, `tsconfig` `paths` and packages). An imported stylesheet is extracted the way the bundler extracts it, so the class names and custom properties it exports are the ones its own CSS uses, and the importer keeps importing it so that CSS is loaded:
+```ts
+// tokens.ts
+export const PRIMARY = 'blue'
+```
+
```ts
// button.css.ts
import { style } from '@vanilla-extract/css'
@@ -40,7 +49,9 @@ A stylesheet that throws while it is evaluated — an import that does not resol
## Ordinary modules
-`style`, `globalStyle` and `keyframes` also resolve in `.ts` / `.tsx`, mapped onto their Devup UI counterparts, and `style([base, { ... }])` keeps composing `base`:
+Named imports of `style`, `globalStyle` and `keyframes` also resolve in `.ts` /
+`.tsx`, mapped onto their Devup UI counterparts. Literal rule objects compile
+there without evaluating the whole module:
```tsx
import { globalStyle, style } from '@vanilla-extract/css'
@@ -49,28 +60,107 @@ export const a = style({ color: 'red' })
globalStyle('body', { margin: 0 })
```
-```tsx
-// output — no import left
-export const a = 'a'
+```text
+import "@devup-ui/react/devup-ui.css";
+export const a = "a";
+;
```
+With an empty theme, the same probe emits:
+
+```text
+/*! devup-ui v1.0.82, | Apache License 2.0 | https://devup-ui.com */@layer b;@layer b{body{margin:0}}.a{color:red}
+```
+
+Generated names depend on the class prefix and extraction state. The
+vanilla-extract API import is removed, but the generated CSS import remains.
+
`globalStyle(selector, rules)` keeps vanilla-extract's two-argument shape; the extractor folds the selector back into the object `globalCss` takes.
-The remaining APIs stay on `@vanilla-extract/css` outside a stylesheet file, because evaluating a module that also contains React components is not possible. A build warning names them:
+When the build knows the composed styles, `style([base, { ... }])` keeps
+non-conflicting declarations and lets the later part win per property,
+selector, breakpoint, and layer, as described in
+[Supported Syntax & Limitations](/docs/limitations). For example:
+
+```ts
+import { style } from '@vanilla-extract/css'
+const base = style({ color: 'red', padding: 8 })
+export const button = style([base, { color: 'blue' }])
```
-[devup-ui] WARNING: '@vanilla-extract/css' keeps styleVariants, createVar because
-devup-ui has no equivalent export, so the package stays a runtime dependency.
+
+Extracted code:
+
+```text
+import "@devup-ui/react/devup-ui.css";
+const base = "a b";
+export const button = "b c";
```
-Moving those calls into a `.css.ts` file — where vanilla-extract wants them anyway — removes the dependency.
+Generated CSS:
-## Namespace imports
+```text
+/*! devup-ui v1.0.82, | Apache License 2.0 | https://devup-ui.com */.c{color:blue}.a{color:red}.b{padding:8px}
+```
-A namespace stands for many named exports whose Devup UI counterparts are renamed (`style` → `css`), which a namespace access cannot express, so it is left alone:
+`button` does not include the conflicting red class. Unknown external classes
+still follow the CSS cascade; class-name string order is not an override rule.
+
+Ordinary-module extraction does **not** execute a vanilla-extract theme contract
+to discover its generated custom properties. Keep contracts and their consuming
+styles together in `.css.ts` / `.css.js`. For example, the stylesheet example
+at the top of this page is not valid if saved as an ordinary `theme.ts` module:
```ts
-// unchanged
+// theme.ts (not a stylesheet file)
+import { createTheme, createThemeContract, style } from '@vanilla-extract/css'
+
+const vars = createThemeContract({ colors: { bg: null } })
+export const light = createTheme(vars, { colors: { bg: 'white' } })
+export const box = style({ background: vars.colors.bg })
```
+The probe locates the call at line 6, column 20 and reports this message:
+
+```text
+`css()` cannot use `vars.colors.bg` at build time: its values must be literals, theme tokens or constants, or be computed from them
+```
+
+The two-file stylesheet example above likewise does not promise that the same
+imports work in an ordinary `.ts` module:
+
+```ts
+// button.ts (not a stylesheet file)
+import { style } from '@vanilla-extract/css'
+
+import { base, vars } from './theme.css'
+import { PRIMARY } from './tokens'
+
+export const button = style([base, { color: PRIMARY, margin: vars.space }])
+```
+
+Even when `theme.css.ts` and `tokens.ts` resolve, the probe locates the
+theme-variable read at line 7, column 23 and reports:
+
+```text
+`css()` cannot use `vars.space` at build time: its values must be literals, theme tokens or constants, or be computed from them
+```
+
+Move that consuming module to `button.css.ts` and export the styles and theme
+values from valid stylesheet modules. Ordinary modules can read resolvable
+literal constants, but that does not make a generated theme object an ordinary
+literal constant. A build-time-known composition must not be confused with a
+theme-variable read the ordinary-module path cannot resolve.
+
+The remaining APIs stay imported from `@vanilla-extract/css` outside a
+stylesheet file. For example, `createVar()` and `styleVariants()` remain calls
+to that package even when a `style()` call in the same module is transformed.
+They are not evaluated by the ordinary-module extraction path.
+
+Moving those calls into a `.css.ts` file — where vanilla-extract wants them anyway — removes the dependency.
+
+## Namespace imports
+
+A namespace stands for many named exports whose Devup UI counterparts are renamed (`style` → `css`), which a namespace access cannot express, so it is left alone:
+
Use named imports to get the rewrite.
diff --git a/apps/landing/src/app/(detail)/docs/overview/page.mdx b/apps/landing/src/app/(detail)/docs/overview/page.mdx
index b186af1b8..a6d49fd90 100644
--- a/apps/landing/src/app/(detail)/docs/overview/page.mdx
+++ b/apps/landing/src/app/(detail)/docs/overview/page.mdx
@@ -31,7 +31,7 @@ Libraries like styled-components and Emotion offer great DX but execute JavaScri
### The Devup UI Solution
-Devup UI eliminates this trade-off entirely. Our Rust-powered preprocessor analyzes your code at build time and handles every CSS-in-JS pattern:
+Devup UI eliminates this trade-off entirely. Our Rust-powered preprocessor analyzes your code at build time; what it cannot know is a CSS variable or a located build error ([supported syntax & limitations](/docs/limitations)):
- **Variables** — Dynamic values become CSS custom properties
- **Conditionals** — Ternary expressions are statically analyzed
diff --git a/e2e/build-errors.spec.ts b/e2e/build-errors.spec.ts
new file mode 100644
index 000000000..a0dd36959
--- /dev/null
+++ b/e2e/build-errors.spec.ts
@@ -0,0 +1,64 @@
+import { expect, test } from '@playwright/test'
+
+const API_SECTIONS: readonly string[] = [
+ 'Style props',
+ 'css / globalCss / keyframes',
+ 'styled',
+ 'Emotion css prop and ClassNames',
+ 'Theme reads',
+ 'Imports and barrels',
+ 'vanilla-extract',
+ 'StyleX',
+ 'Plugins and config',
+]
+
+test.describe('Build Errors reference', () => {
+ // Read the exported SSR HTML without vinext's client router, at a width
+ // where the docs sidebar is shown.
+ test.use({
+ javaScriptEnabled: false,
+ viewport: { width: 1440, height: 900 },
+ })
+
+ test('serves the page heading and canonical URL', async ({ page }) => {
+ const response = await page.goto('/docs/build-errors')
+
+ expect(response?.status()).toBe(200)
+ await expect(page.locator('.markdown-body h1')).toHaveText('Build Errors')
+ await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
+ 'href',
+ /^(?:https:\/\/devup-ui\.com)?\/docs\/build-errors$/,
+ )
+ })
+
+ test('has a section for each API, in order', async ({ page }) => {
+ await page.goto('/docs/build-errors')
+
+ const headings = await page.locator('.markdown-body h2').allTextContents()
+
+ expect(
+ headings.filter((heading) => API_SECTIONS.includes(heading)),
+ `h2 headings: ${JSON.stringify(headings)}`,
+ ).toEqual(API_SECTIONS)
+ })
+
+ test('docs sidebar links the page right after the limitations page', async ({
+ page,
+ }) => {
+ await page.goto('/docs/build-errors')
+
+ const sidebarLink = page.locator(
+ 'a[href="/docs/limitations"] + a[href="/docs/build-errors"]',
+ )
+ await expect(sidebarLink).toBeVisible()
+ await expect(sidebarLink).toHaveText('Build Errors')
+ })
+
+ test('limitations Build errors section links the page', async ({ page }) => {
+ await page.goto('/docs/limitations')
+
+ await expect(
+ page.locator('h2#build-errors ~ p a[href="/docs/build-errors"]'),
+ ).toHaveText('Build Errors')
+ })
+})
diff --git a/e2e/exported-routes.ts b/e2e/exported-routes.ts
index 5c8edd2bc..31993a757 100644
--- a/e2e/exported-routes.ts
+++ b/e2e/exported-routes.ts
@@ -63,6 +63,7 @@ export const EXPECTED_EXPORTED_ROUTES = [
'/docs/api/style-props',
'/docs/api/text',
'/docs/api/v-stack',
+ '/docs/build-errors',
'/docs/core-concepts/nm-base',
'/docs/core-concepts/no-dependencies',
'/docs/core-concepts/optimize-css',
@@ -80,6 +81,7 @@ export const EXPECTED_EXPORTED_ROUTES = [
'/docs/figma-and-theme-integration/devup-figma-plugin',
'/docs/figma-and-theme-integration/devup-json',
'/docs/installation',
+ '/docs/limitations',
'/docs/migration/overview',
'/docs/migration/styled-components',
'/docs/migration/stylex',
diff --git a/libs/extractor/README.md b/libs/extractor/README.md
index 9f43037ea..706dcfd13 100644
--- a/libs/extractor/README.md
+++ b/libs/extractor/README.md
@@ -1,28 +1,40 @@
## Extractor
-jsx to css extractor
+Build-time JSX to CSS extractor. Static styles become atomic classes; dynamic
+style values are passed through CSS variables on the native element.
### Example
-Before
+Standalone input (with the build plugin configured):
```tsx
-
- Hello World
-
+import { Box } from '@devup-ui/react'
+
+function Example({ variable }: { variable: 'left' | 'right' }) {
+ return
+ Hello World
+
+}
```
-After
+Extracted code from an isolated run with an empty theme (`{}`) and the default
+prefix. Generated class and variable names depend on extraction state and prefix.
```tsx
-
- Hello World
-
+import "@devup-ui/react/devup-ui.css";
+function Example({ variable }: {
+ variable: "left" | "right";
+}) {
+ return
+ Hello World
+
;
+}
+```
+
+Generated CSS:
+
+```css
+/*! devup-ui v1.0.82, | Apache License 2.0 | https://devup-ui.com */.a{background:red}.b{color:white}.c{margin:8px}.d{padding:8px}.e{text-align:var(--f)}
```
```mermaid
diff --git a/libs/sheet/tests/docs_breakpoints.rs b/libs/sheet/tests/docs_breakpoints.rs
new file mode 100644
index 000000000..a1d0650fc
--- /dev/null
+++ b/libs/sheet/tests/docs_breakpoints.rs
@@ -0,0 +1,137 @@
+use sheet::theme::Theme;
+use std::{
+ error::Error,
+ io::{Error as IoError, ErrorKind},
+};
+
+#[test]
+fn typography_breakpoint_indices_match_default_theme() -> Result<(), Box
> {
+ let docs =
+ include_str!("../../../apps/landing/src/app/(detail)/docs/devup/typography/page.mdx");
+ let defaults = Theme::default().breakpoints;
+
+ let documented: Vec<(usize, u16)> = docs
+ .lines()
+ .filter_map(|line| line.trim().strip_prefix("- Index "))
+ .map(|entry| -> Result<_, Box> {
+ let (index, value) = entry.split_once(':').ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "typography breakpoint entry must contain an index and value",
+ )
+ })?;
+ let pixels = value
+ .trim()
+ .strip_prefix('`')
+ .and_then(|value| value.split_once("px`").map(|(pixels, _)| pixels))
+ .ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "typography breakpoint value must be pixels in inline code",
+ )
+ })?;
+ Ok((index.parse()?, pixels.parse()?))
+ })
+ .collect::>()?;
+
+ assert_eq!(
+ documented,
+ defaults.into_iter().enumerate().collect::>(),
+ "public typography breakpoint indices must match Theme::default()"
+ );
+ Ok(())
+}
+
+#[test]
+fn documented_breakpoint_ranges_match_default_theme() -> Result<(), Box> {
+ let docs =
+ include_str!("../../../apps/landing/src/app/(detail)/docs/devup/breakpoints/page.mdx");
+ let defaults = Theme::default().breakpoints;
+ let table = docs
+ .split_once("")
+ .and_then(|(_, body)| body.split_once(""))
+ .map(|(body, _)| body)
+ .ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "breakpoints page must contain a ranges table body",
+ )
+ })?;
+
+ let documented: Vec<(usize, u16, Option)> = table
+ .split("")
+ .skip(1)
+ .map(|row| -> Result<_, Box> {
+ let cells = row
+ .split("")
+ .skip(1)
+ .map(|cell| {
+ cell.split_once("")
+ .map(|(value, _)| value.trim())
+ .ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "breakpoint table cell must have a closing tag",
+ )
+ })
+ })
+ .collect::, _>>()?;
+ let mut cells = cells.into_iter();
+ let index = cells
+ .next()
+ .ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "breakpoint row must contain an index",
+ )
+ })?
+ .parse()?;
+ let range = cells.nth(1).ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "breakpoint row must contain a range",
+ )
+ })?;
+ let (start, end) = if let Some(start) = range.strip_suffix("px+") {
+ (start, None)
+ } else {
+ let (start, end) = range.split_once("px - ").ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "breakpoint range must have pixel bounds",
+ )
+ })?;
+ let end = end.strip_suffix("px").ok_or_else(|| {
+ IoError::new(ErrorKind::InvalidData, "range end must use pixels")
+ })?;
+ (start, Some(end.parse()?))
+ };
+ Ok((index, start.parse()?, end))
+ })
+ .collect::>()?;
+
+ let expected: Vec<_> = defaults
+ .iter()
+ .copied()
+ .enumerate()
+ .map(|(index, start)| {
+ let end = defaults
+ .get(index + 1)
+ .map(|next| {
+ next.checked_sub(1).ok_or_else(|| {
+ IoError::new(
+ ErrorKind::InvalidData,
+ "default breakpoint range must have a positive upper boundary",
+ )
+ })
+ })
+ .transpose()?;
+ Ok::<_, IoError>((index, start, end))
+ })
+ .collect::>()?;
+ assert_eq!(
+ documented, expected,
+ "public breakpoint range indices and bounds must match Theme::default()"
+ );
+ Ok(())
+}
diff --git a/packages/eslint-plugin/src/rules/css-utils-literal-only/README.md b/packages/eslint-plugin/src/rules/css-utils-literal-only/README.md
index c9416e32c..ba8f84b5c 100644
--- a/packages/eslint-plugin/src/rules/css-utils-literal-only/README.md
+++ b/packages/eslint-plugin/src/rules/css-utils-literal-only/README.md
@@ -4,7 +4,7 @@ Enforce that CSS utility functions only use values known at build time in devup-
## Rule Details
-This rule ensures that CSS utility functions (`css`, `globalCss`, `keyframes`, `createGlobalStyle`) from devup-ui, and the StyleX functions devup-ui compiles (`create`, `keyframes`, `defineVars`, `defineConsts`, `createTheme`, `createThemeContract`, `positionTry`, `viewTransitionClass`), only receive values the build knows. They have no element to set a CSS variable on, so a value known only at runtime is a build error.
+This rule ensures that CSS utility functions (`css`, `globalCss`, `keyframes`, `createGlobalStyle`) from devup-ui, and the StyleX functions devup-ui compiles (`create`, `keyframes`, `defineVars`, `defineConsts`, `createTheme`, `createThemeContract`, `positionTry`, `viewTransitionClass`), only receive values the build knows. They have no element to set a CSS variable on, so a value known only at runtime is a [build error](https://devup-ui.com/docs/build-errors).
It checks the values of every rule object they take, in any argument, and the interpolations of CSS text, written as a template argument or a tagged template (`` css`color: ${color};` ``). A part `css()` composes as a class (`css(base, { m: 1 })`), and a condition choosing between parts, are read at runtime and not checked. `styled()` sets a CSS variable on the element it renders, so its values are not checked either.
diff --git a/packages/react/README.md b/packages/react/README.md
index ede5459d6..9a7fbf7fa 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -103,7 +103,7 @@ The Turbopack ranges overlap, so the direct-API result is effectively parity wit
Devup UI is a CSS in JS preprocessor that does not require runtime.
Devup UI eliminates the performance degradation of the browser through the CSS in JS preprocessor.
-We develop a preprocessor that considers all grammatical cases.
+What the build cannot know is kept as a CSS variable or reported as a [located build error](https://devup-ui.com/docs/build-errors); see [supported syntax & limitations](https://devup-ui.com/docs/limitations).
```tsx
const before =
@@ -111,7 +111,7 @@ const before =
const after =
```
-Variables are fully supported.
+Variables become CSS variables.
```tsx
const before =
@@ -126,7 +126,7 @@ const after = (
)
```
-Various expressions and responsiveness are also fully supported.
+Conditions and responsive arrays compile too.
```tsx
const before = b ? 'yellow' : variable]} />