Theming

The Forest theme, its six schemes, the gray ramp, colored edges, per-section accents and the font hooks.

The Forest theme

A Zoo UI theme changes color and type, and nothing else. Forest points Nuxt UI's semantic colors at its own ramps, sets Inter and JetBrains Mono, and gives light pages a paper ground under white surfaces. Radius, spacing, shadows, motion, focus rings and every component's geometry stay exactly as Nuxt UI ships them.

Everything downstream is Nuxt UI's own logic: which shade paints a hover, picking the 400 shade in dark mode, the alpha steps behind soft and subtle variants. The theme chooses colors; it doesn't re-specify components.

Nuxt UI colorForest ramp
primarythe scheme's accent
neutral, secondarythe --pine-* grays (Forest's secondary is muted, not a second hue)
success, infoa pair chosen per scheme
warningamber
errorclay red

On top of the colors, Forest sets a value in the mono face where it reads as data: badges, keyboard keys, and table headers (as a small uppercase kicker). Alerts get a striped edge, and toasts a colored left edge with no progress bar.

Schemes

A scheme is a set of raw ramps: the grays, the accent, and a success and info pair. Import one after the layer's stylesheet and every component repaints. Use the picker in the header to try them on this page.

SchemeFileSuccessInfo
Pine · Teal (default)schemes/teal.cssgreensky
Slate · Cobaltschemes/slate-cobalt.cssgreencyan
Graphite · Violetschemes/graphite-violet.cssgreenblue
Paper · Oliveschemes/paper-olive.csstealblue
Ink · Indigoschemes/ink-indigo.cssgreencyan
Ember · Amberschemes/ember.cssgreenblue

Success and info are per scheme because they carry fixed meanings and must never read as the primary. Forest's accents span most of the hue wheel, so no single green and blue stay clear of all of them; each scheme's pair sits at least 30° of OKLCH hue away from its accent.

The gray ramp

Every surface, text color and border comes from one gray ramp, --pine-*, which is Nuxt UI's neutral. The default is slightly green-tinted; each scheme brings its own. These swatches read the live variables, so they follow the scheme you pick.

50
100
200
300
400
500
600
700
800
900
950

--pine-*

The accent ramp, --teal-* in every scheme (the name is historical), is Nuxt UI's primary:

50
100
200
300
400
500
600
700
800
900
950

--teal-*

Use Nuxt UI's semantic utilities (text-muted, bg-elevated, border-default) rather than the ramps directly, so your markup follows the scheme and dark mode on its own.

The page ground. Nuxt UI paints the light page with --ui-bg, the same white its cards, inputs and overlays use. Forest paints the page with --pine-50 and leaves --ui-bg white, so everything Nuxt UI draws reads as raised. Let the body show through: a wrapper painted bg-default hides the ground.

Colored edges

Zoo UI's one addition to Nuxt UI's surfaces is a 3px edge in the surface's color: on the left of inline surfaces, on the top of overlays. No color, no edge.

SurfaceHowEdge
Card<ZCard color="…">left
Dialog<ZModal color="…">top
Side drawer<ZSlideover color="…">top
Bottom drawer<ZDrawer color="…">top
ToastuseToast().add({ color }) (Forest)left
Banner<UAlert color="…" variant="outline"> (Forest)left, striped

The four wrappers belong to the base, so a product without Forest has them too, in its own colors. Each adds only color and passes every other prop, v-model, event and slot straight to the Nuxt UI component.

No color

A plain Nuxt UI card.

primary

A card with the primary edge.

success

A card with the success edge.

warning

A card with the warning edge.

error

A card with the error edge.

<script setup lang="ts">
const colors = ['primary', 'success', 'warning', 'error'] as const
</script>

<template>
  <div class="w-full grid sm:grid-cols-2 gap-3">
    <ZCard>
      <p class="font-semibold">No color</p>
      <p class="text-sm text-muted">A plain Nuxt UI card.</p>
    </ZCard>
    <ZCard v-for="c in colors" :key="c" :color="c">
      <p class="font-semibold capitalize">{{ c }}</p>
      <p class="text-sm text-muted">A card with the {{ c }} edge.</p>
    </ZCard>
  </div>
</template>

Per-section accent

A product can paint each section in its own hue. Put data-accent="<name>" on a container, and everything inside reads six roles instead of a fixed color:

RoleFor
--accentthe solid: an icon tile, a band's rule, a colored edge
--accent-ontext or an icon on the solid
--accent-strongtitles and labels in the accent
--accent-softa band or panel tint
--accent-mutedchips
--accent-linea rule or border

The base defines the roles; your product supplies the hues. A hue is eight stops, --a-50 to --a-950, set under [data-accent='<name>'] in your CSS. A name with no hue reads as your primary. Components read the roles with arbitrary values (bg-(--accent), border-(--accent-line)), and the colored-edge wrappers take color="accent". Overlays carry the section's accent with them when they portal to <body>, so a dialog opened from a violet section has a violet edge.

Billing

This card's edge is the section's accent.

<template>
  <section data-accent="violet" class="w-full rounded-md bg-(--accent-soft) p-4 flex flex-col gap-3">
    <p class="text-sm font-semibold text-(--accent-strong)">Billing</p>
    <ZCard color="accent">
      <p class="text-sm">This card's edge is the section's accent.</p>
    </ZCard>
  </section>
</template>

<style>
/* The product supplies each hue's stops; here, Tailwind's violet. */
[data-accent='violet'] {
  --a-50: var(--color-violet-50); --a-100: var(--color-violet-100); --a-200: var(--color-violet-200);
  --a-400: var(--color-violet-400); --a-500: var(--color-violet-500); --a-600: var(--color-violet-600);
  --a-800: var(--color-violet-800); --a-950: var(--color-violet-950);
}
</style>

light-dark()

Each role carries its light and its dark value in one declaration, light-dark(var(--a-600), var(--a-400)), so your CSS needs no .dark rules. Your own tokens can do the same:

app/assets/css/main.css
@theme {
  --color-val-input: light-dark(var(--color-sky-900), var(--color-sky-200));
}

This needs color-scheme declared where the tokens live. Tailwind's production build compiles light-dark() into variables it defines only under a color-scheme declaration, and the color-mode module only toggles a dark class. So the base declares :root { color-scheme: light } and .dark { color-scheme: dark }. Without it, such a token resolves in development and comes out empty in production.

Typefaces and the font hooks

Forest's faces are Inter for the interface and display, and JetBrains Mono for code and values. Both are under the SIL Open Font License and self-hosted from fontsource packages the theme depends on, so nothing is fetched from a font CDN.

Each role is two hooks, a family and its OpenType feature set:

HookForest's default
--zoo-font-sans"Inter Variable", then system faces
--zoo-font-display"Inter Variable", then system faces
--zoo-font-mono"JetBrains Mono Variable", then system monos
--zoo-features-sansnormal
--zoo-features-displaynormal
--zoo-features-mono"calt" 0: code ligatures off, so pasted text reads as typed

Every place the theme sets a face reads the hooks: Tailwind's font-sans, font-mono and font-display, and the defaults for the page and for code, kbd and pre. Switching a family switches its feature set with it. The defaults sit in a cascade layer, so any unlayered declaration of your own wins, whatever the import order:

app/assets/css/main.css
@import "@zooui/theme-forest/assets/css/forest.css";

:root {
  --zoo-font-sans: "Acme Sans", system-ui, sans-serif;
  --zoo-features-sans: "ss01";
}

A licensed font package

A licensed face is best shipped as its own package that sets all six hooks. For example, a package named @acme/licensed-fonts could ship a commercial face family and its feature sets. A product licensed for it imports its CSS after the theme, and the faces and their sets replace Inter and JetBrains Mono together:

app/assets/css/main.css
@import "@zooui/theme-forest/assets/css/forest.css";
@import "@zooui/theme-forest/schemes/ink-indigo.css";
@import "@acme/licensed-fonts/fonts.css";

Zoo UI itself never depends on such a package; the hooks are the whole contract.