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:
@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.
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:
@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:
<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>
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:
{/* 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.
| Want | Instead of | Use |
|---|---|---|
| A rounder control | rounded-full | [--cml-radius-control:999px] |
| The brand color | bg-blue-600 text-white | bg-cml-accent text-cml-accent-fg |
| Muted text | text-gray-500 | text-cml-fg-secondary |
| A change in one skin only | in-data-[style=retro]:… | an @scope rule in CSS |
The Scoped overrides guide shows how to write those rules.
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:
@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
@scoperules, not inin-data-[style=…]variants - Colors you add come from tokens, so they follow the mode and the brand