Theming

PreviousNext

Design tokens in OKLCH, mapped to Tailwind utilities. One stylesheet controls every component.

A ZardUI theme is a set of CSS variables in src/styles.css . Each one is mapped to a Tailwind color through @theme inline , so --primary becomes bg-primary . Redefining the variable under .dark — or under any container class — retints every component underneath it, with no rebuild and no per-component configuration.

There are three ways to get a theme:

  • Run the CLI.zard-cli init writes the whole file and asks which base color you want — see Installation .
  • Copy the CSS. The complete stylesheet is in How it works , explained line by line.
  • Build your own. The theme generator gives you every token with a live preview and exports finished CSS.

How it works

This is the whole file, exactly as zard-cli init writes it for the Neutral base color. Paste it into src/styles.css and every component is themed.

src/styles.css
Copy
@layer ng-icon, theme, base, components, utilities;
@import 'tailwindcss';
@import './app/shared/core/css/zard';
@plugin "tailwindcss-animate";

@custom-variant dark (&:is(.dark *));

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --destructive-foreground: oklch(0.205 0 0);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-destructive-foreground: var(--destructive-foreground);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }

  input[type="number"]::-webkit-inner-spin-button,
  input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield; /* Added for general compatibility */
  }
}

::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

::-webkit-scrollbar-thumb {
  background: var(--muted-foreground);
  border-radius: 5px;
}

::-webkit-scrollbar-track {
  border-radius: 5px;
  background: var(--muted);
}

Line by line

Nine parts, in the order they appear above. Each one earns its place — the last line of every entry says what breaks without it.

  1. @layer ng-icon, theme, base, components, utilities;

    Cascade layer order

    Declaring the layers up front fixes their priority. ng-icon comes first, so it has the lowest weight — @ng-icons/core ships its rules inside @layer ng-icon.

    Without it
    Icon styles win over your utilities: text-primary on an <ng-icon> stops changing its color.
  2. @import 'tailwindcss';

    TailwindCSS itself

    Pulls in preflight, the default theme and the utility engine. Everything below extends it.

    Without it
    No utility class resolves at all.
  3. @import './app/shared/core/css/zard';

    ZardUI's core stylesheet

    Defines the data-* custom variants (data-open, data-checked, data-selected, data-disabled, data-active, data-horizontal, data-vertical) the accordion and caret keyframes, and no-scrollbar, which combobox and command rely on. scroll-fade and shimmer are not here — they ship in utilities.css, a separate registry item. The path follows the core alias in components.json.

    Without it
    The 64 data-*: classes the library relies on silently compile to nothing — open/closed states, checked switches and selected menu items stop reacting. animate-caret-blink and no-scrollbar also disappear.
  4. @plugin "tailwindcss-animate";

    Enter/exit animations

    Provides animate-in, animate-out, fade-*, zoom-* and slide-in-from-*.

    Without it
    Dialogs, sheets, popovers and dropdowns appear and vanish with no transition — over 50 classes across the library stop resolving.
  5. @custom-variant dark (&:is(.dark *));

    Class-based dark mode

    Binds the dark: variant to a .dark ancestor instead of prefers-color-scheme. That is what lets ZardDarkMode flip the theme by toggling one class on <html>.

    Without it
    dark: follows the OS setting only. The theme toggle stops working and the .dark block below never applies.
  6. :root { --background: oklch(1 0 0); … }

    Light values

    The raw color of every token, in OKLCH. Plain CSS variables — no Tailwind involved yet.

    Without it
    Every token resolves to nothing and components render unstyled.
  7. .dark { --background: oklch(0.145 0 0); … }

    Dark overrides

    Redefines the same variables under .dark. Only the values change — no component knows which mode is active.

    Without it
    Dark mode keeps the light palette.
  8. @theme inline { --color-primary: var(--primary); … }

    Tokens → Tailwind utilities

    Turns each token into a color utility: --color-primary is what makes bg-primary exist. inline is the important word — it makes Tailwind emit var(--primary) in the output instead of copying the value, so the .dark override still applies at runtime.

    Without it
    Without the block, bg-primary does not exist. Without inline, the light value gets baked into the CSS and dark mode has no effect.
  9. @layer base { * { @apply border-border outline-ring/50; } … }

    Global defaults

    Gives every element the themed border color and focus ring, and paints body with bg-background text-foreground.

    Without it
    Borders fall back to currentColor, focus rings lose their theme color, and the page background stays white in dark mode.
Why OKLCH and not HSL or HEX?
Lightness is perceptual: the same lightness value looks equally light at any hue, so a palette stays balanced when you shift the hue. Interpolation stays clean — color-mix() and opacity modifiers do not drift toward grey the way HSL does.

Convention

Tokens come in pairs: a surface and the text that sits on it. The background suffix is dropped, so --primary is the surface and --primary-foreground is the text on top of it.

--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);

Every pair, rendered

These are real utilities on real elements — the contrast you see is the contrast you get.

--primary--primary-foreground
Aa

light 17.16:1

Aa

dark 14.22:1

--secondary--secondary-foreground
Aa

light 16.42:1

Aa

dark 14.48:1

--muted--muted-foreground
Aa

light 4.34:1 — below the 4.5:1 AA threshold for body text

Aa

dark 5.83:1

--accent--accent-foreground
Aa

light 16.42:1

Aa

dark 14.48:1

--destructive--destructive-foreground
Aa

light 4.71:1

Aa

dark 6.24:1

--card--card-foreground
Aa

light 19.79:1

Aa

dark 17.16:1

--popover--popover-foreground
Aa

light 19.79:1

Aa

dark 17.16:1

--sidebar--sidebar-foreground
Aa

light 18.96:1

Aa

dark 17.16:1

Contrast ratios are computed from the Neutral preset. * marks a pair below the 4.5:1 WCAG AA threshold for body text — use it for large or secondary text, not for body copy.

Tokens or raw utility classes?

Prefer tokens. They react to .dark and to scoped overrides on their own:

<div class="bg-background text-foreground">
  <!-- Your ZardUI components here -->
</div>

Raw Tailwind colors work too, but you own every dark-mode variant by hand and lose scoped theming entirely:

<div class="bg-zinc-950 dark:bg-white text-zinc-50 dark:text-zinc-950">
  <!-- Your ZardUI components here -->
</div>

Token reference

All 32 tokens, with their Neutral values in both modes. Expand Used by under a token to see which components read it. The list comes from a scan of the library source, so a token marked not used yet really is unused today.

Showing 32 of 32 tokens · light values for Neutral

Base

Canvas, text and the four action colors.

  • --backgroundoklch(1 0 0)

    App canvas. @layer base applies it to body.

    bg-backgroundtext-backgroundring-background
    Used by 12 components

    avatar, button, calendar, card, field, input-group, kbd, layout, sheet, switch, tabs, tooltip

  • --foregroundoklch(0.145 0 0)

    Default text color, applied to body next to --background.

    text-foregroundbg-foregroundring-foregroundfill-foreground
    Used by 18 components

    alert, alert-dialog, badge, breadcrumb, button, card, command, dialog, empty, input, input-group, input-otp, sheet, spinner, switch, tabs, toggle, tooltip

  • --primaryoklch(0.205 0 0)

    Main action color — filled buttons, checked switches, progress fill.

    bg-primarytext-primaryborder-primaryring-primaryfill-primary
    Used by 18 components

    alert-dialog, avatar, badge, button, calendar, carousel, checkbox, empty, field, input-group, input-otp, item, progress, radio-group, sheet, slider, switch, tabs

  • --primary-foregroundoklch(0.985 0 0)

    Text and icons drawn on top of --primary.

    text-primary-foregroundbg-primary-foreground
    Used by 9 components

    avatar, badge, button, calendar, checkbox, input-group, radio-group, sheet, switch

  • --secondaryoklch(0.97 0 0)

    Low-emphasis action color for secondary buttons and badges.

    bg-secondary
    Used by 3 components

    badge, button, input-group

  • --secondary-foregroundoklch(0.205 0 0)

    Text drawn on top of --secondary.

    text-secondary-foreground
    Used by 3 components

    badge, button, input-group

  • --mutedoklch(0.97 0 0)

    Quiet surface — skeletons, separators, keyboard chips, table headers.

    bg-muted
    Used by 20 components

    alert-dialog, avatar, button, button-group, calendar, card, command, dialog, empty, input-group, item, kbd, progress, skeleton, slider, spinner, table, tabs, toggle, toggle-group

  • --muted-foregroundoklch(0.556 0 0)

    Secondary text — descriptions, placeholders, helper copy. The most used token in the library.

    text-muted-foreground
    Used by 32 components

    accordion, alert, alert-dialog, avatar, breadcrumb, button-group, calendar, card, combobox, command, date-picker, dialog, dropdown, empty, field, form, input, input-group, input-otp, item, kbd, layout, menu, popover, select, separator, sheet, slider, table, tabs, textarea, toggle-group

  • --accentoklch(0.97 0 0)

    Hover and active surface for list items — menus, selects, trees, calendars.

    bg-accent
    Used by 6 components

    badge, calendar, dropdown, layout, menu, select

  • --accent-foregroundoklch(0.205 0 0)

    Text drawn on top of --accent.

    text-accent-foreground
    Used by 6 components

    badge, calendar, command, dropdown, menu, select

  • --destructiveoklch(0.577 0.245 27.325)

    Error and danger states — delete buttons, invalid fields, error rings.

    bg-destructivetext-destructiveborder-destructivering-destructive
    Used by 18 components

    alert, alert-dialog, badge, button, checkbox, command, dropdown, field, input, input-group, input-otp, menu, radio-group, select, sheet, switch, textarea, toggle

  • --destructive-foregroundoklch(0.985 0 0)

    Text drawn on top of a solid --destructive surface. Matches --primary-foreground in every theme.

    text-destructive-foregroundNot used yet

Surfaces

Layers that sit above the canvas.

  • --cardoklch(1 0 0)

    Raised surface for cards and alerts.

    bg-card
    Used by 2 components

    alert, card

  • --card-foregroundoklch(0.145 0 0)

    Text drawn on top of --card.

    text-card-foreground
    Used by 2 components

    alert, card

  • --popoveroklch(1 0 0)

    Floating surface — dialogs, dropdowns, menus, selects, command palette.

    bg-popover
    Used by 7 components

    alert-dialog, command, dialog, dropdown, menu, popover, select

  • --popover-foregroundoklch(0.145 0 0)

    Text drawn on top of --popover.

    text-popover-foreground
    Used by 7 components

    alert-dialog, command, dialog, dropdown, menu, popover, select

Form

Borders, inputs and focus rings.

  • --borderoklch(0.922 0 0)

    Default border color. @layer base applies border-border to every element.

    border-borderbg-borderfill-border
    Used by 14 components

    avatar, badge, button, carousel, command, dropdown, field, input-group, item, layout, resizable, select, separator, table

  • --inputoklch(0.922 0 0)

    Border of form controls, and their fill at 30% opacity.

    border-inputbg-input
    Used by 15 components

    button, button-group, card, checkbox, command, input, input-group, input-otp, radio-group, select, sheet, switch, tabs, textarea, toggle

  • --ringoklch(0.708 0 0)

    Focus ring. @layer base sets outline-ring/50 globally; components add ring-ring/50.

    ring-ringborder-ringoutline-ring
    Used by 19 components

    accordion, badge, button, calendar, card, checkbox, input, input-group, input-otp, item, radio-group, resizable, select, sheet, slider, switch, tabs, textarea, toggle

Charts

Categorical palette for data visualization.

  • --chart-1oklch(0.809 0.105 251.813)

    Categorical series 1 for data visualization. Defined for parity with shadcn chart blocks — no ZardUI component consumes it yet.

    Not used yet
  • --chart-2oklch(0.623 0.214 259.815)

    Categorical series 2 for data visualization. Defined for parity with shadcn chart blocks — no ZardUI component consumes it yet.

    Not used yet
  • --chart-3oklch(0.546 0.245 262.881)

    Categorical series 3 for data visualization. Defined for parity with shadcn chart blocks — no ZardUI component consumes it yet.

    Not used yet
  • --chart-4oklch(0.488 0.243 264.376)

    Categorical series 4 for data visualization. Defined for parity with shadcn chart blocks — no ZardUI component consumes it yet.

    Not used yet
  • --chart-5oklch(0.424 0.199 265.638)

    Categorical series 5 for data visualization. Defined for parity with shadcn chart blocks — no ZardUI component consumes it yet.

    Not used yet

Sidebar

A self-contained scale for app shells.

  • --sidebaroklch(0.985 0 0)

    Sidebar surface, kept separate from --background so the two can contrast.

    bg-sidebar
    Used by 1 component

    layout

  • --sidebar-foregroundoklch(0.145 0 0)

    Text drawn on top of --sidebar.

    text-sidebar-foreground
    Used by 1 component

    layout

  • --sidebar-primaryoklch(0.205 0 0)

    Active sidebar entry. Defined by the CLI; not consumed by any component yet.

    Not used yet
  • --sidebar-primary-foregroundoklch(0.985 0 0)

    Text on top of --sidebar-primary. Defined by the CLI; not consumed yet.

    Not used yet
  • --sidebar-accentoklch(0.97 0 0)

    Hover surface for sidebar entries.

    bg-sidebar-accent
    Used by 1 component

    layout

  • --sidebar-accent-foregroundoklch(0.205 0 0)

    Text on top of --sidebar-accent. Defined by the CLI; not consumed yet.

    Not used yet
  • --sidebar-borderoklch(0.922 0 0)

    Divider between the sidebar and the content area.

    border-sidebar-border
    Used by 1 component

    layout

  • --sidebar-ringoklch(0.708 0 0)

    Focus ring inside the sidebar.

    ring-sidebar-ring
    Used by 1 component

    layout

Radius & scale

--radius is the single knob for corner rounding. The four utilities below are derived from it in @theme inline , so changing one value reshapes every component.

--radius :
  • rounded-sm6px--radius-sm: calc(var(--radius) - 4px)
  • rounded-md8px--radius-md: calc(var(--radius) - 2px)
  • rounded-lg10px--radius-lg: var(--radius)
  • rounded-xl14px--radius-xl: calc(var(--radius) + 4px)

Base colors

Five neutral scales ship with the CLI. They differ only in the hue mixed into the greys — pick one, then adjust --primary to taste.

Live previewNeutral · light
BadgeSecondaryDestructive

Muted copy, for the text that supports the rest.

Build your own theme →
src/styles.css Copy
:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --destructive-foreground: oklch(0.205 0 0);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}

Dark mode

Dark mode is the .dark block and nothing else. No component knows which mode is active: they all read the same token names, and the values change underneath them.

Two pieces make it work — @custom-variant dark (&:is(.dark *)) binds the dark: variant to a class instead of the OS setting, and something has to put that class on <html> . The ZardDarkMode service does that, and persists the choice.

Set up dark mode →

Customizing

Three things you are likely to want: a new color, a token overridden for one part of the app, and a theme that follows a route.

Adding a new color

Declare the raw value, override it under .dark , then map it — the same three steps every built-in token goes through.

src/styles.css Copy
:root {
  --warning: oklch(0.84 0.16 84);
  --warning-foreground: oklch(0.28 0.07 46);
}

.dark {
  --warning: oklch(0.41 0.11 46);
  --warning-foreground: oklch(0.99 0.02 95);
}

@theme inline {
  --color-warning: var(--warning);
  --color-warning-foreground: var(--warning-foreground);
}
<div class="bg-warning text-warning-foreground">Warning message</div>

Overriding tokens in one scope

Because @theme inline emits var(--primary) instead of a baked-in color, any container can redefine a token for its subtree.

src/styles.css Copy
.theme-brand {
  --primary: oklch(0.55 0.22 264);
  --primary-foreground: oklch(0.98 0.01 264);
  --radius: 1rem;
}
<section class="theme-brand">
  <!-- Every ZardUI component in here picks up the scoped values. -->
  <button z-button>Brand button</button>
</section>

A theme per route

Put the override class on a layout component's host and everything rendered through its outlet inherits it.

src/app/marketing/marketing.layout.ts Copy
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-marketing-layout',
  imports: [RouterOutlet],
  // The class carries the token overrides declared in src/styles.css.
  host: { class: 'theme-brand' },
  template: `<router-outlet />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketingLayout {}
src/app/app.routes.ts Copy
import type { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'marketing',
    loadComponent: () => import('./marketing/marketing.layout').then(m => m.MarketingLayout),
    children: [{ path: '', loadComponent: () => import('./marketing/home.page').then(m => m.HomePage) }],
  },
];

Other color formats

The raw tokens hold plain CSS colors, so RGB, HSL and HEX all work. Keep the @theme inline mapping as it is.

src/styles.css Copy
:root {
  /* OKLCH — what ZardUI ships */
  --primary: oklch(0.205 0 0);

  /* RGB */
  --secondary: rgb(244 244 245);

  /* HSL */
  --accent: hsl(240 5% 96%);

  /* HEX */
  --muted: #f4f4f5;
}

Need the values? The colors page lists the full Tailwind palette in OKLCH, HEX, RGB and HSL.

Troubleshooting

Every entry below is a real failure mode of the setup on this page, with the line that causes it.

Cause
@custom-variant dark (&:is(.dark *)); is missing, so dark: still maps to prefers-color-scheme.
Fix
Add the @custom-variant line above :root, and make sure something puts the .dark class on <html>.
Cause
@theme was used instead of @theme inline. Tailwind copied the light values into the output.
Fix
Use @theme inline. It emits var(--primary) instead of the resolved color, so .dark can still win.
Cause
The core stylesheet is not imported, so the data-* custom variants do not exist.
Fix
Add @import './app/shared/core/css/zard'; right after @import 'tailwindcss';.
Cause
@plugin "tailwindcss-animate"; is missing, so animate-in and friends do not resolve.
Fix
Add the @plugin line and install tailwindcss-animate.
Cause
The @layer order is missing or ng-icon is not first, so icon rules outrank your utilities.
Fix
Keep @layer ng-icon, theme, base, components, utilities; as the very first line of the file.
Cause
The variable was declared inside a component stylesheet or a nested selector instead of :root.
Fix
Declare raw values in :root / .dark, and scope overrides deliberately with a container class.
github iconwhatsapp icondiscord iconX icon

Made with ❤ in Brazil. Open source and available on GitHub .