// preview in
saved for every page

Tailwind CSS

Use Camaleo next to Tailwind CSS v4. Put the layers in the right order, turn the tokens into utilities and let the skin keep its job.

// you'll learn

  • Where Camaleo's layers go among Tailwind's, and why the order matters
  • How to use the --cml-* tokens as Tailwind theme values
  • How to reach a part of a component through its data-slot
  • What to style with utilities, and what to leave to the skin

Camaleo doesn't need Tailwind: the skins are plain CSS. If your app already uses Tailwind v4, the two work together with a few lines in your main stylesheet. There is no plugin or preset to install.

Order the layers

All of Camaleo's CSS lives in four layers: cml.tokens, cml.components, cml.style and cml.preferences. Tailwind v4 uses theme, base, components and utilities. Layers declared first lose, so declare the whole order once, before the imports:

app/globals.css
@layer theme, base, cml, components, utilities;

@import "tailwindcss";
@import "@camaleo/styles/base.css";
@import "@camaleo/styles/glass.css";

With this order:

  • Tailwind's reset (Preflight, in base) sits below Camaleo, so it never flattens a skin's buttons or fields.
  • Your utilities sit above Camaleo, so className="mt-6 w-full" on a component wins over its own spacing and width.
  • Unlayered CSS you write still beats everything.
CAREFUL

Without the first line, the import order decides: Camaleo's layers come after Tailwind's and beat even your utilities, so className="w-full" on a Button may seem to do nothing.

Turn tokens into theme values

Map the tokens you want as utilities in a @theme inline block. The names below use a cml- prefix so they never clash with Tailwind's palette:

app/globals.css
@theme inline {
  --color-cml-surface: var(--cml-surface);
  --color-cml-raised: var(--cml-surface-raised);
  --color-cml-fg: var(--cml-fg);
  --color-cml-fg-secondary: var(--cml-fg-secondary);
  --color-cml-border: var(--cml-border);
  --color-cml-accent: var(--cml-accent);
  --color-cml-accent-fg: var(--cml-accent-fg);
  --color-cml-accent-text: var(--cml-accent-text);
  --color-cml-danger: var(--cml-danger);
  --radius-cml-control: var(--cml-radius-control);
  --radius-cml-surface: var(--cml-radius-surface);
  --font-cml-body: var(--cml-font-body);
  --font-cml-display: var(--cml-font-display);
}

Now bg-cml-surface, text-cml-fg-secondary, border-cml-border, rounded-cml-surface and font-cml-display exist, and each one follows the skin, the mode and the brand of the nearest StyleProvider:

app/page.tsx
<aside className="rounded-cml-surface border border-cml-border bg-cml-raised p-6">
  <h2 className="font-cml-display text-cml-fg">Your plan</h2>
  <p className="text-cml-fg-secondary">Renews on 12 November.</p>
</aside>
TIP

inline matters. Without it, Tailwind declares --color-cml-surface on :root, where it resolves once, to the root skin. With inline, the utility writes var(--cml-surface) itself, so an island in another skin gets its own value.

Style the parts with data-slot

Every part of every component has a stable data-slot, and state comes as data attributes (data-active, data-checked, data-disabled…). Tailwind's data variants reach them directly:

app/settings.tsx
{/* A state of the element itself */}
<Tab value="billing" className="data-active:font-semibold">Billing</Tab>

{/* A part inside a component */}
<Card className="[&_[data-slot=card-title]]:text-lg">…</Card>

{/* A token, for this element only */}
<Button className="[--cml-radius-control:0px]">Export</Button>

Each component page lists its slots, state attributes and tokens under Styling hooks.

Don't fight the skin

Utilities are for your layout: margins, widths, grid placement, gaps and the page around the components. Those don't change from one skin to another.

The look of a component (colors, radius, borders, shadows, fonts) belongs to the skin. If you paint a Button with bg-blue-600 rounded-full shadow-lg, it stops changing with the skin, ignores the brand and dark mode, and its contrast is no longer checked by the library's tests.

WantInstead ofUse
A rounder controlrounded-full[--cml-radius-control:999px]
The brand colorbg-blue-600 text-whitebg-cml-accent text-cml-accent-fg
Muted texttext-gray-500text-cml-fg-secondary
A change in one skin onlyin-data-[style=retro]:…an @scope rule in CSS

The Scoped overrides guide shows how to write those rules.

CAREFUL

A variant like in-data-[style=retro]:uppercase matches any retro ancestor, including one outside a nested island in another skin. Camaleo's own skin rules use @scope ([data-style="retro"]) to ([data-style]), which stops at the next island.

Dark mode

The tokens already switch with the mode (they use light-dark()), so bg-cml-surface needs no dark: variant. Tailwind's dark: follows the system by default. If you set mode yourself and need dark: for your own colors, point the variant at data-mode:

app/globals.css
@custom-variant dark (&:where([data-mode=dark], [data-mode=dark] *));

This variant only sees an explicit mode="dark". With mode="system" or no mode, keep Tailwind's default, or use tokens.

Before you ship it

  • @layer theme, base, cml, components, utilities; comes before every @import
  • Tokens are mapped with @theme inline, not plain @theme
  • Utilities on components only handle layout and spacing
  • Skin-specific changes live in @scope rules, not in in-data-[style=…] variants
  • Colors you add come from tokens, so they follow the mode and the brand
Was this page helpful? · Edit this page on GitHub