Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0c1829a
refactor(theme)!: rename the semantic tokens after the accent roles
benjamincanac Sep 28, 2026
a9b10c9
fix(theme): keep the default border visible on filled surfaces in dar…
benjamincanac Sep 28, 2026
54cf86f
docs: rename the semantic tokens applied in AdsCarbon
benjamincanac Sep 28, 2026
531682d
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 28, 2026
f596381
chore(theme): keep the v4 token classes until the docs move over
benjamincanac Sep 28, 2026
7d4b636
Merge remote-tracking branch 'origin/docs/theming-pages' into refacto…
benjamincanac Sep 28, 2026
f961fc9
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 28, 2026
5e28dee
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 28, 2026
d19b4e9
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 28, 2026
bf17a7a
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
990f3be
refactor(theme)!: rename the page background utilities after the surf…
benjamincanac Sep 29, 2026
c73cb44
chore(theme): keep ring-offset-bg in the temporary bridge
benjamincanac Sep 29, 2026
7ed2e0c
refactor(theme): bring back border-muted for borders on a fill, solid…
benjamincanac Sep 29, 2026
87fa181
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
516b16c
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
e9dc6c1
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
8c14833
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
11d8b68
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
0e3edc1
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
38147c7
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
b8b02b5
test(colors): use the renamed surface token
benjamincanac Sep 29, 2026
d1649fc
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
10e4062
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
56d9a37
Merge branch 'docs/theming-pages' into refactor/theme-tokens
benjamincanac Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 10 additions & 8 deletions .github/contributing/theme-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ A theme is a plain object wrapped in `defineTheme`. It never reads module option

`defineTheme` checks `compoundVariants` and `defaultVariants` against `variants`, and keeps their values typed as the variant's values, which inference alone widens to `string`. A theme that builds on another uses `extendTheme(base, {...})` instead, typed after `defuFn`: its values win, a function receives the base value and returns the new one, and `compoundVariants` concatenate. Type a function's parameter from the base (`(prev: typeof input.variants.variant) => ...`) so the variant values survive.

Write each class out whole. A class built at runtime, from a template literal (`` `${hover}bg-elevated` ``), a helper that maps or rewrites classes, or a string with escaped quotes (`'content-[\'*\']'`), never reaches Tailwind's scanner and gets no CSS. Use backticks for a class that holds quotes, and give a helper that rewrites classes its results as literals, like `replaceFocus` in `input.ts`. The `theme classes` test in `test/utils/theme-slots.spec.ts` fails on any class the themes resolve to that isn't spelled out in `src/runtime/theme`.
Write each class out whole. A class built at runtime, from a template literal (`` `${hover}bg-soft` ``), a helper that maps or rewrites classes, or a string with escaped quotes (`'content-[\'*\']'`), never reaches Tailwind's scanner and gets no CSS. Use backticks for a class that holds quotes, and give a helper that rewrites classes its results as literals, like `replaceFocus` in `input.ts`. The `theme classes` test in `test/utils/theme-slots.spec.ts` fails on any class the themes resolve to that isn't spelled out in `src/runtime/theme`.

```ts
import { defineTheme } from '../utils/theme'
Expand Down Expand Up @@ -76,7 +76,7 @@ export default defineTheme({
color: colorVariant({ base: '' }),
variant: {
solid: { base: 'text-accent-foreground bg-accent hover:bg-accent-hover' },
outline: { base: 'ring ring-inset ring-accent-border text-accent-soft-foreground bg-accent-surface' },
outline: { base: 'ring ring-inset ring-accent-border-strong text-accent-soft-foreground bg-accent-surface' },
soft: { base: 'text-accent-soft-foreground bg-accent-soft' }
},
size: {
Expand Down Expand Up @@ -122,21 +122,23 @@ Always use semantic colors, never Tailwind palette colors:
### Text Colors
- `text-default` - Primary text
- `text-muted` - Secondary text
- `text-dimmed` - Tertiary/placeholder text
- `text-faint` - Tertiary/placeholder text
- `text-highlighted` - Emphasized text
- `text-inverted` - Text on dark backgrounds

### Background Colors
- `bg-default` - Primary background
- `bg-elevated` - Elevated surface (cards, dropdowns)
- `bg-accented` - Subtle accent background
- `bg-surface` - Primary background
- `bg-soft` - Raised surface (cards, dropdowns)
- `bg-soft-hover` - Hover of a raised surface
- `bg-tint` - Hovered and highlighted items
- `bg-inverted` - Inverted (dark) background

### Border Colors
- `border-default` - Standard borders
- `ring-default` - Focus rings
- `ring-accented` - Accented rings
- `ring-strong` - Field outlines
- `divide-default` - Dividers
- `outline-focus` - Focus outline of a component without a `color` prop

### Accent Tokens
The `color` prop accepts `primary`, `secondary`, `success`, `info`, `warning`, `error` and `neutral`. Colored classes read the scoped color through `accent` roles, never through an alias name and never with an opacity modifier, so every color and the neutral scope can tune each state:
Expand All @@ -145,7 +147,7 @@ The `color` prop accepts `primary`, `secondary`, `success`, `info`, `warning`, `
- `bg-accent-hover` - Hover of a solid background
- `bg-accent-soft` / `bg-accent-soft-hover` - Tinted background and its hover
- `text-accent-soft-foreground` - Text on a tinted background
- `ring-accent-border` / `ring-accent-border-soft` / `ring-accent-border-muted` - Colored borders
- `ring-accent-border` / `ring-accent-border-strong` - Colored borders
- `outline-accent-focus` - Focus outline
- `bg-accent-surface` - Resting background of an outlined element
- `bg-accent-tint` - Light tint on large surfaces
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Options:
## Key Conventions

- **Conventional commits**: All commit messages must follow [conventional commits](https://conventionalcommits.org) (e.g. `fix(Button): resolve hover state`, `feat(Modal): add fullscreen prop`).
- **Semantic colors**: Use `text-default`, `bg-elevated`, etc. β€” never raw Tailwind palette colors like `text-gray-500`.
- **Semantic colors**: Use `text-default`, `bg-soft`, etc. β€” never raw Tailwind palette colors like `text-gray-500`.
- **`Soon` badge on docs headings**: PRs that introduce a new feature or fix often add `:badge{label="Soon" class="align-text-top"}` to the relevant docs heading. This is intentional: the docs site redeploys on merge, but the feature only ships on the next npm release β€” the badge bridges that gap. Do NOT flag this as inconsistent in reviews. See [documentation.md](.github/contributing/documentation.md) for details.
- **Two build adapters**: Build-side changes (templates, auto-imports, icons, component detection, build plugins) must be checked against both adapters: `src/module.ts` for Nuxt, `src/unplugin.ts` and `src/plugins/*` for Vue. Shared logic belongs in `src/utils/`. In code that runs from the published build, only the two entry files may resolve paths from `import.meta.url`, since bundled files can land in any output chunk. Everything else anchors on `runtimeDir`.

Expand Down Expand Up @@ -100,7 +100,7 @@ Load these based on your task. **Do not load all files at once** β€” only load w
| Computed ui | Always use `computed(() => tv(theme, overrides.value)(...))` for reactive theming, with `const overrides = useComponentOverrides((ui: X['AppConfig']['ui']) => ui.<name>)`: it reads the entry from `app.config.ui` and from each `<UTheme>` around the component, which the engine stacks nearest last, and carries `<UTheme unstyled>`, the app's merge config and prefix to the engine and types the variant values the app adds. Read the rest of the `ui` config (icons) through `const appConfig = useThemeConfig() as X['AppConfig']`, never `useAppConfig()` |
| Theme defaults | Wrap raw props with `useComponentProps(name, _props, theme)` to resolve the priority chain (explicit prop > `<UTheme :props>` > `<UTheme :props>` `'*'` > `app.config.ui.<name>.defaultVariants` > `app.config.ui.defaultVariants` > `withDefaults`). `props.ui` holds the component's own `ui` only, a `<UTheme :ui>` reaches the engine as a level of overrides β€” read `props.ui?.<slot>` in templates. The theme's `defaultVariants` still only feed `tv()` class resolution; the proxy reads them to apply `'*'` only where the default is `primary` / `md`. Pass the **raw** `_props` (not the proxy) to `useFormField` / `useFieldGroup` / `useAvatarGroup` so their injection precedence (closer context wins) stays correct. |
| Form/group fallback | When consuming `size` / `color` / `highlight` from `useFormField`, `useFieldGroup`, or `useAvatarGroup`, always fall back to the proxy in `tv()` calls: `size: size.value ?? props.size`, `color: color.value ?? props.color`, `highlight: highlight.value ?? props.highlight`. This gives the full precedence `explicit > group/formField > <UTheme :props> > <UTheme :props> '*' > app.config.ui.<name>.defaultVariants > app.config.ui.defaultVariants > withDefaults`. Without the `?? props.X` fallback, `<UTheme :props>` is silently dropped when the closer context (FormField/FieldGroup/AvatarGroup) is absent. The group itself provides what was set for it, read from `useComponentProps(name, _props)` without `theme`, so the `'*'` defaults aren't passed down as its own value and a child's own `<UTheme :props>` key still wins. |
| Semantic colors | Use `text-default`, `bg-elevated`, etc. - never Tailwind palette |
| Semantic colors | Use `text-default`, `bg-soft`, etc. - never Tailwind palette |
| Logical properties (RTL) | Use logical utilities (`ms/me`, `ps/pe`, `start/end`, `text-start/end`, `border-s/e`, `rounded-s/e`) not physical (`ml/mr`, `left/right`, `text-left/right`) so components work in RTL by default. `transform`/`cursor`/gradients/transitions need explicit `rtl:` counterparts. See [theme-structure.md](.github/contributing/theme-structure.md#logical-properties-rtl). |
| Reka UI props | Use `reactivePick` + `useForwardProps(source, emits?)` from `composables/useForwardProps` to forward props (proxy-aware; reka-ui's `useForwardProps` / `useForwardPropsEmits` filter out `<UTheme :props>` defaults) |
| Form components | Use `useFormField` and `useFieldGroup` composables |
Expand Down
2 changes: 1 addition & 1 deletion docs/app/components/AdsCarbon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ onMounted(() => {
@reference "../assets/css/main.css";

.carbon :deep(#carbonads) {
@apply relative border border-default rounded-md hover:bg-elevated/50 w-full transition-colors min-h-[220px] p-2;
@apply relative border border-default rounded-md hover:bg-tint w-full transition-colors min-h-[220px] p-2;

.carbon-img {
@apply flex justify-center w-full;
Expand Down
56 changes: 52 additions & 4 deletions docs/content/docs/1.getting-started/3.migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ A class written as a plain string in `variants` or `compoundVariants` used to ap
compoundVariants: [{
color: 'neutral',
variant: 'outline',
- class: 'ring-default hover:bg-accented'
- class: 'ring-default hover:bg-soft-hover'
+ class: {
+ base: 'ring-default hover:bg-accented'
+ base: 'ring-default hover:bg-soft-hover'
+ }
}]
}
Expand Down Expand Up @@ -268,7 +268,7 @@ This can't be applied by a find and replace, and no codemod covers it: the selec

Component themes no longer repeat their classes for every color. The `color` prop sets a `--ui-accent` variable on the element, and the classes read it through new `accent` utilities such as `bg-accent`, `text-accent-foreground`, `bg-accent-soft` and `ring-accent-border`. [Accent](/docs/getting-started/theme/design-system#accent) lists the ones you can rely on.

Every color, `neutral` included, renders as before, with six exceptions. A linked Banner hovers at 75% like Button instead of 90%. Callout text uses the color itself instead of its 600 and 300 shades. A Timeline paints its pending indicators itself, so an item's `avatar.color` no longer tints them. The items of a CheckboxGroup with `variant="table"` take a Checkbox's own `color` instead of the group's. A colored Calendar highlights and hovers its cells at 15% of the color instead of 20%, like every other soft hover. A checked item of the `card` and `table` variants of Checkbox, CheckboxGroup and RadioGroup has a border in the color itself instead of the color at 50%. For `neutral`, that border goes from `inverted` at 50% to the full `inverted` color, much darker in light mode and lighter in dark mode.
Every color, `neutral` included, renders as before, with six exceptions, and the few [semantic tokens](#semantic-tokens) changes. A linked Banner hovers at 75% like Button instead of 90%. Callout text uses the color itself instead of its 600 and 300 shades. A Timeline paints its pending indicators itself, so an item's `avatar.color` no longer tints them. The items of a CheckboxGroup with `variant="table"` take a Checkbox's own `color` instead of the group's. A colored Calendar highlights and hovers its cells at 15% of the color instead of 20%, like every other soft hover. A checked item of the `card` and `table` variants of Checkbox, CheckboxGroup and RadioGroup has a border in the color itself instead of the color at 50%. For `neutral`, that border goes from `inverted` at 50% to the full `inverted` color, much darker in light mode and lighter in dark mode.

```diff
-<button class="… text-inverted bg-primary hover:bg-primary/75 …">
Expand All @@ -282,7 +282,7 @@ Every color, `neutral` included, renders as before, with six exceptions. A linke
- An app that declares its own `--color-accent` in `@theme` overrides the one Nuxt UI declares, and every component follows it. Rename yours.
- The classes each variant takes for a color moved from the theme's `compoundVariants` into its `variants`, except where a color changes more than the accent: DropdownMenu and ContextMenu items, whose color is optional per item, and Calendar, ChatMessage and the prose Callout, whose `neutral` is a design of its own. Your `app.config.ui` variants now come after them, so an override that used to lose silently applies, like `variants.variant.solid: { base: 'bg-red-500' }` on Button, which lost to `bg-primary`.

`neutral` is a color like the others. A new `--ui-neutral` variable, shade `900` of its palette in light mode and `50` in dark mode, goes through the same classes, and the neutral scope points the accent roles at the surface tokens such as `bg-elevated`, `text-default` and `ring-accented`, so it keeps its current look. Callout, inline Code, Calendar and ChatMessage keep their own neutral classes.
`neutral` is a color like the others. A new `--ui-neutral` variable, shade `900` of its palette in light mode and `50` in dark mode, goes through the same classes, and the neutral scope points the accent roles at the surface tokens such as `bg-soft`, `text-default` and `ring-strong`, so it keeps its current look. Callout, inline Code, Calendar and ChatMessage keep their own neutral classes.

Neutral text, rings and borders now follow `--ui-neutral` where they used `--ui-text-highlighted` or `--ui-border-inverted`, like the active link of Tabs or NavigationMenu and the focus ring of Input. In light mode the values are the same. In dark mode a solid neutral component, and those neutral accents, use shade `50` of the palette instead of pure white.

Expand All @@ -297,6 +297,54 @@ Setting `--ui-accent` through `class` also gives a component a color outside the
This can't be applied by a find and replace: selectors on color classes have to be read one by one, and the color name no longer appears in a component's classes, only in its `--ui-accent` class.
::

### Semantic tokens

The surface tokens are renamed after the accent roles, so a word means the same thing on both sides: `bg-soft` is always neutral, `bg-accent-soft` follows `color`. Their CSS variables take the property they style as a prefix, like `--ui-bg-soft` or `--ui-border-strong`.

```diff
-<div class="bg-default text-dimmed">
+<div class="bg-surface text-faint">
-<div class="bg-elevated hover:bg-accented">
+<div class="bg-soft hover:bg-soft-hover">
-<div class="hover:bg-elevated/50">
+<div class="hover:bg-tint">
-<p class="text-toned">
+<p class="text-default">
-<div class="ring ring-accented">
+<div class="ring ring-strong">
-<span class="ring-2 ring-bg">
+<span class="ring-2 ring-surface">
```

The same renames apply with any variant or modifier, `hover:bg-accented/75` becoming `hover:bg-soft-hover/75`, to `ring-*` and `divide-*`, and to the gradient stops of a background token, `from-default` becoming `from-surface`. `bg-elevated` with another opacity keeps it, `bg-elevated/75` becomes `bg-soft/75`. The utilities in the page's background color take the same word, `ring-bg`, `border-bg`, `fill-bg` and `stroke-bg` becoming `ring-surface`, `border-surface`, `fill-surface` and `stroke-surface`.

| v4 | v5 |
| --- | --- |
| `--ui-bg` | `--ui-bg-surface` |
| `--ui-bg-elevated` | `--ui-bg-soft` |
| `--ui-bg-accented` | `--ui-bg-soft-hover` |
| `--ui-text` | `--ui-text-default` |
| `--ui-text-dimmed` | `--ui-text-faint` |
| `--ui-text-toned` | Removed, `--ui-text-default` |
| `--ui-border` | `--ui-border-default` |
| `--ui-border-accented` | `--ui-border-strong` |

`--ui-bg-muted`, `--ui-bg-inverted`, `--ui-text-muted`, `--ui-text-highlighted`, `--ui-text-inverted`, `--ui-border-muted` and `--ui-border-inverted` keep their names. Two tokens are new. `--ui-bg-tint` is behind `bg-tint`, the fill of hovered and highlighted items, and `--ui-focus` behind `outline-focus`, the focus outline of components without a `color` prop. Both are unset by default: the utilities mix `--ui-bg-soft` at 50% and `--ui-primary` at 25% on the element, so a subtree that overrides those follows, and setting the token overrides the mix. `ring-offset-bg` becomes `ring-offset-surface`, and the unused `divide-bg` and `outline-default` utilities are removed.

The tokens, with `--ui-radius`, `--ui-container` and `--ui-header-height`, move from `@layer theme` to `@layer base` at zero specificity, like the palettes, so an override of yours in `@layer base` now wins over them. An override in `@layer theme` now loses: move it to `base` or out of any layer.

A few components look slightly different:

- The `tint` role of the colors goes from 10% to 5% of the color, half of `soft` like `bg-tint` for neutral. Colored items highlighted in DropdownMenu and ContextMenu, the `soft` and `subtle` Alert and a hovered prose Card are lighter.
- With `color="neutral"`, the components whose border comes from the `border` accent role use `--ui-border-muted`, the border meant for a fill. The `subtle` Button, Badge, Kbd and Alert are lighter in light mode than with `--ui-border-accented` and the same in dark mode. The `outline` Alert and ChatMessage, and the `subtle` ChatMessage, are the same in light mode and one shade lighter in dark mode than with `--ui-border`.
- The soft hover of `color="neutral"`, like a hovered `soft` Button, is `--ui-bg-soft-hover` itself instead of 75% of it.
- The FileUpload dropzone, hovered or with a file dragged over it, uses `bg-tint`, 50% of `--ui-bg-soft` instead of 25%.
- `text-toned` merges into `text-default`, one shade darker in light mode and lighter in dark mode, on BlogPost, ChangelogVersion, Empty, PageCard, PageCTA, PricingPlan, PricingTable, User and the prose Field.

::note
The class and variable renames can be applied by a find and replace on whole names, so that `--ui-text` doesn't catch `--ui-text-muted`, as long as `bg-elevated/50` is replaced before `bg-elevated`. An override of `--ui-text-toned` in your CSS has no token of its own left: move it to `--ui-text-default` if it should still apply. An override of an old variable that the find and replace misses is silently ignored. Classes built at runtime, like `` `bg-${tone}` ``, have to be checked by hand.
::

### `theme.colors` option

The `theme.colors` option is removed. Components accept a fixed set of colors, `primary`, `secondary`, `success`, `info`, `warning`, `error` and `neutral`, which you still map to any Tailwind palette, now in your CSS (see [`ui.colors` option](#uicolors-option)). Themes no longer need to know your aliases at build time. Trimming the list no longer saves any CSS either, since a color now costs one class.
Expand Down
Loading
Loading