---
title: "Restyle with tokens"
description: "Re-theme every packaged component with the --bk-* tokens, follow Nuxt UI and color mode with no setup, scope a change to one family, or style the data-* hooks directly."
canonical_url: "https://nuxt-backend.dev/docs/ui/customize/theming"
---
# Restyle with tokens

> Re-theme every packaged component with the --bk-* tokens, follow Nuxt UI and color mode with no setup, scope a change to one family, or style the data-* hooks directly.

Every shipped component renders semantic, class-free markup where each element carries a `data-*` attribute, and every color/radius resolves through custom properties. Styling is therefore three independent layers you can mix:

1. **--bk-* tokens** — one override re-themes every component.
2. **Per-family tokens** (`--auth-*`) — scope a change to one component family.
3. **data-* hooks** — style the markup directly.

## The `--bk-*` tokens

Defined at zero specificity (`:where(...)`) on every component root, so any app selector wins:

<table>
<thead>
  <tr>
    <th>
      Token
    </th>
    
    <th>
      Role
    </th>
    
    <th>
      Default
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        --bk-accent
      </code>
    </td>
    
    <td>
      Primary actions, links, focus
    </td>
    
    <td>
      <code>
        light-dark(#116632, #4ade80)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-on-accent
      </code>
    </td>
    
    <td>
      Text on accent surfaces
    </td>
    
    <td>
      <code>
        light-dark(#ffffff, #052912)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-ink
      </code>
    </td>
    
    <td>
      Body text
    </td>
    
    <td>
      <code>
        inherit
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-muted
      </code>
    </td>
    
    <td>
      Secondary text
    </td>
    
    <td>
      <code>
        color-mix(currentColor 55%)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-edge
      </code>
    </td>
    
    <td>
      Borders
    </td>
    
    <td>
      <code>
        color-mix(currentColor 18%)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-surface
      </code>
    </td>
    
    <td>
      Card backgrounds
    </td>
    
    <td>
      <code>
        color-mix(currentColor 5%)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-error
      </code>
    </td>
    
    <td>
      Error messages
    </td>
    
    <td>
      <code>
        light-dark(#a1302d, #f87171)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-ok
      </code>
    </td>
    
    <td>
      Success messages
    </td>
    
    <td>
      <code>
        light-dark(#116632, #4ade80)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-warn
      </code>
    </td>
    
    <td>
      Warnings
    </td>
    
    <td>
      <code>
        light-dark(#6d5600, #facc15)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-radius
      </code>
    </td>
    
    <td>
      Corners of cards, banners and messages
    </td>
    
    <td>
      <code>
        8px
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-radius-control
      </code>
    </td>
    
    <td>
      Corners of buttons and inputs
    </td>
    
    <td>
      <code>
        9999px
      </code>
      
       (pills)
    </td>
  </tr>
</tbody>
</table>

The defaults carry no brand colour: green means go (the accent and the ok tone), yellow warns, red is an error. Each fixed colour is a `light-dark()` pair, a darker shade for light pages and a lighter one for dark pages, so the same signal stays readable as text on either background. The derived tokens mix from `currentColor`, so they follow your text colour.

On a wide-gamut screen the four signals go further than sRGB can: a display-P3 screen gets each one in `color(display-p3 …)`, a Rec. 2020 screen in `color(rec2020 …)`, at the same lightness and hue with more chroma, so the contrast measured on the sRGB pair holds. Every other screen keeps the sRGB value. An override of yours replaces every tier at once.

The package stylesheet never sets `color-scheme`; your app owns it. Declare `color-scheme: light dark` (or `dark`) on the root and every pair follows the page. With no scheme declared, each pair takes its light value. With `@nuxtjs/color-mode` or Nuxt UI, the module does it for you: see below.

Declare the scheme in a stylesheet, not only in a `<meta name="color-scheme">` tag or a `style` attribute. A production build rewrites `light-dark()` for the browsers that predate it (Vite minifies CSS with Lightning CSS for Chrome 111, Safari 16.4 and Firefox 114), and the rewritten pairs follow only the `color-scheme` rules they find in CSS. For a page that declares none, the module sets the light half as the default, at the root, in the lowest layer, so `nuxt dev` and the build agree and a rule of yours wins wherever it sits.

```css
/* app.css — every nuxt-backend surface picks this up */
:root {
  --brand: #6d5dfc;
}

[data-auth], [data-invitation], [data-consent], [data-gift], [data-pricing],
[data-history], [data-usage], [data-credits],
[data-settings], [data-profile], [data-security] {
  --bk-accent: var(--brand);
  --bk-on-accent: white;
  --bk-radius: 10px;
}
```

## On Nuxt UI and color mode

The module looks for two modules once every module has loaded, and bridges to each one it finds. The bridge is part of one generated stylesheet the module adds right after its own (with the `light-dark()` default above), in the same cascade layer as the package's, so your overrides still win.

- **@nuxt/ui**: the neutrals, the borders and the card corners take Nuxt UI's values, so the packaged pages sit in your app's surfaces and follow its dark mode. The signals stay the package's own (green, yellow, red, wide-gamut where the screen allows), and controls stay pills.<table>
<thead>
  <tr>
    <th>
      Token
    </th>
    
    <th>
      Nuxt UI value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        --bk-muted
      </code>
    </td>
    
    <td>
      <code>
        var(--ui-text-muted)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-edge
      </code>
    </td>
    
    <td>
      <code>
        var(--ui-border)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-surface
      </code>
    </td>
    
    <td>
      <code>
        var(--ui-bg-muted)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        --bk-radius
      </code>
    </td>
    
    <td>
      <code>
        calc(var(--ui-radius) * 2)
      </code>
      
      , the radius of Nuxt UI's cards
    </td>
  </tr>
</tbody>
</table>

<br />

`--bk-ink` stays `inherit`: Nuxt UI's text colour already reaches it. These are the variables Nuxt UI documents for theming, so theme them the way its docs (and Docus's) recommend, in your app's CSS, and the packaged pages change with the rest of the app:```css [app/app.css]
:root {
  --ui-radius: 0.5rem;
  --ui-border: var(--color-neutral-300);
}
.dark {
  --ui-border: var(--color-neutral-700);
}
```
- **@nuxtjs/color-mode** (Nuxt UI installs it): the component roots take `color-scheme` from the class it puts on `<html>`, so the `light-dark()` defaults follow the mode a visitor picked, not the OS setting. The class names are the ones color-mode renders, with every source of its `classPrefix` and `classSuffix` resolved: your config, the options another module passes it (Nuxt UI sets `classSuffix: ''`), or its own default (`-mode` up to color-mode 3, none from 4).

Inside the page, the nearest element with one of those classes decides: Nuxt UI's `.dark` and `.light`, or color-mode's own. Nuxt UI switches its neutrals on them on any element, so a `<section class="dark">` on a light page gets the dark signals along with its dark surfaces. Browsers without `@scope` follow the `<html>` class alone.

To keep a token of your own, override it as usual: a rule of yours outside a layer wins over the bridge too.

## `data-*` families

<table>
<thead>
  <tr>
    <th>
      Family
    </th>
    
    <th>
      Component / page
    </th>
    
    <th>
      Root
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        data-auth
      </code>
    </td>
    
    <td>
      <code>
        <AuthForm>
      </code>
      
       + login page
    </td>
    
    <td>
      <code>
        [data-auth='form']
      </code>
      
      , <code>
        [data-auth='page']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-invitation
      </code>
    </td>
    
    <td>
      <code>
        <AcceptInvitation>
      </code>
      
       + page
    </td>
    
    <td>
      state-valued root (<code>
        loading
      </code>
      
      /<code>
        ready
      </code>
      
      /…)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-consent
      </code>
    </td>
    
    <td>
      <code>
        <AgentConsent>
      </code>
      
       + page
    </td>
    
    <td>
      state-valued root (<code>
        loading
      </code>
      
      /<code>
        ready
      </code>
      
      /…)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-gift
      </code>
    </td>
    
    <td>
      <code>
        <GiftClaimBanner>
      </code>
    </td>
    
    <td>
      <code>
        [data-gift='banner']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-pricing
      </code>
    </td>
    
    <td>
      <code>
        <PricingTable>
      </code>
      
       + page
    </td>
    
    <td>
      <code>
        [data-pricing='table']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-history
      </code>
    </td>
    
    <td>
      <code>
        <BillingHistory>
      </code>
    </td>
    
    <td>
      <code>
        [data-history='root']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-usage
      </code>
    </td>
    
    <td>
      <code>
        <UsageHistory>
      </code>
    </td>
    
    <td>
      <code>
        [data-usage='root']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-credits
      </code>
    </td>
    
    <td>
      <code>
        <CreditsLowBanner>
      </code>
    </td>
    
    <td>
      <code>
        [data-credits='banner']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-settings
      </code>
    </td>
    
    <td>
      <code>
        <WorkspaceSettings>
      </code>
      
       + page
    </td>
    
    <td>
      <code>
        [data-settings='root']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-profile
      </code>
    </td>
    
    <td>
      <code>
        <ProfileSettings>
      </code>
      
       + page
    </td>
    
    <td>
      <code>
        [data-profile='root']
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        data-security
      </code>
    </td>
    
    <td>
      <code>
        <SecuritySettings>
      </code>
      
       + page
    </td>
    
    <td>
      <code>
        [data-security='root']
      </code>
    </td>
  </tr>
</tbody>
</table>

Families use prefix selectors for groups (`[data-auth^='step-']`, `[data-settings^='section-']`, `[data-auth^='input-']`) and state attributes for variants: `[data-pricing='plan'][data-current]`, `[data-current]` on sessions, `[data-editing]` on passkey rows, `[data-tone='ok'|'error'|'warn']` on messages and badges, `[data-intent='subscribe'|'upgrade'|'downgrade'|'switch'|'cancel'|'uncancel'|'sign-in']` on pricing actions (`upgrade`/`downgrade` when both prices are known, `switch` otherwise), `[data-status]` on billing-history orders (the provider's order status — `paid`, `refunded`, …).

Form fields are wrapped in real labels, never placeholder-only: `[data-auth^='label-']` on the sign-in form, `[data-settings='label-create']` around the create-workspace input.

Messages take their accessibility role from their tone. Errors are `role="alert"`, announced at once; success and notice messages (a settings confirmation, the low-credits banner) are `role="status"` with `aria-live="polite"`. A component that is loading or submitting carries `aria-busy="true"`. Style on `data-tone` or the element's `data-*` name, not on the role, and keep the role if you replace a message through a slot. Every button and link the components render gets a `:focus-visible` ring in `--bk-accent`.

The exhaustive per-element vocabulary is the stylesheet itself — each family has one file (`auth.css`, `invitation.css`, `consent.css`, `gift.css`, `pricing.css`, `history.css`, `usage.css`, `credits.css`, `settings.css`, `profile.css`, `security.css`) barreled, after the shared `tokens.css`, into `ui.css`.

## Packaging

- The module adds the stylesheet to every app, ahead of the app's own CSS, in the `components.backend` cascade layer. A rule of yours outside a layer always wins over it, with no specificity to match, and so does a Tailwind utility; Tailwind's reset does not, so the packaged buttons and inputs keep their look on a Tailwind app.
- The same holds for a reset of your own, and that cuts the other way: outside a layer, `* { margin: 0; padding: 0 }` or `button { background: none }` wins over the package too and strips its buttons and inputs. Keep resets and element rules in a layer below `components`, as Tailwind keeps its reset in `base`: wrap them in `@layer base { … }`, or import the file into the layer.```css [app/assets/main.css]
@import './reset.css' layer(base);

@layer base {
  button { font: inherit; }
}
```
- `--auth-*` properties still theme `<AuthForm>` alone and derive from `--bk-*` (`--auth-accent: var(--bk-accent)`).

The playground demonstrates exactly this: the [SaaS demo pages](https://nuxt-backend.dev/playground/saas/pricing) are the packaged components under the site's own palette (one token override in its `app.css`), while the [Vanilla pages](https://nuxt-backend.dev/playground/vanilla/pricing) show what an app gets without one. This site runs on Nuxt UI, so that is the module's bridge onto Nuxt UI's tokens.


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
