Skip to content

Theming

Learn how the color system works and how to customize the look and feel of Ember Dashboard.

OKLCh Color Tokens

All colors are defined as CSS custom properties using the OKLCh color space. OKLCh provides perceptually uniform lightness, making it easier to create consistent color palettes. The tokens are defined in src/app/globals.css.

:root {
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0.002 230);
  --background: oklch(0.985 0.002 230);
  --foreground: oklch(0.155 0.015 230);
  --card: oklch(1 0 0);
  --muted: oklch(0.96 0.005 230);
  --muted-foreground: oklch(0.556 0.015 230);
  --border: oklch(0.922 0.005 230);
  /* ... more tokens */
}

The OKLCh format is oklch(lightness chroma hue) where lightness is 0–1, chroma is 0–0.4, and hue is 0–360 degrees. Tailwind CSS v4 maps these tokens through the @theme inline block at the top of globals.css.

Changing the Primary Color

To change the primary color, update the --primary token in both the :root (light) and .dark blocks. For example, to switch from teal to blue:

/* Light mode */
:root {
  --primary: oklch(0.55 0.175 250);          /* hue 160 → 250 (blue) */
  --primary-foreground: oklch(0.985 0.002 230);
  --ring: oklch(0.55 0.175 250);
}

/* Dark mode */
.dark {
  --primary: oklch(0.65 0.19 250);
  --primary-foreground: oklch(0.09 0.015 170);
  --ring: oklch(0.65 0.19 250);
}

The change propagates everywhere bg-primary, text-primary, or ring-primary is used — buttons, links, active states, charts, and sidebar accents all update automatically.

Semantic Color Tokens

The design system uses semantic names rather than raw color values. Each token has a foreground counterpart for text that sits on top of it:

TokenPurpose
--primaryBrand color, buttons, active states
--secondarySecondary actions, subtle backgrounds
--mutedMuted backgrounds, code blocks
--accentHover states, highlights
--destructiveError states, delete actions
--successSuccess indicators, positive trends
--warningWarning badges, caution states
--chart-1 to --chart-5Chart color palette (5 colors)
--sidebar-*Sidebar-specific tokens (dark sidebar in light mode)

Dark Mode

Dark mode is implemented by toggling a .dark class on the <html> element. The ThemeProvider component in src/components/theme-provider.tsx manages the state and persists the preference to localStorage. The Tailwind dark variant is configured in globals.css:

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

This allows you to use dark:bg-slate-900 classes in your components, and the .dark block in globals.css overrides all semantic tokens for dark mode.

Using the Theme Provider

The useTheme hook provides access to the current theme and a setter:

"use client";

import { useTheme } from "@dashboardpack/core/providers/theme-provider";

export function MyComponent() {
  const { theme, setTheme } = useTheme();

  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Current theme: {theme}
    </button>
  );
}

Supported values are "light", "dark", and "system". The system option follows the user's OS preference and reacts to changes in real time.

Theme Customizer

Ember Dashboard includes a live Theme Customizer accessible via the palette icon in the header. It opens as a slide-out drawer panel where you can preview every change in real time without refreshing the page. The Customizer controls the following settings:

  • Color Presets — 6 built-in palettes: Default (Teal), Ocean, Sunset, Forest, Berry, and Slate. Each preset swaps the full set of OKLCh tokens at runtime, instantly recoloring every component, chart, and sidebar accent across the entire dashboard.
  • Density — Three levels: Compact, Default, and Comfortable. Adjusts spacing, padding, and font sizes globally via CSS custom properties, letting you fit more data on screen or give content room to breathe.
  • Layout — Choose between the default Sidebar layout (fixed left navigation) or a Horizontal Top-Nav layout where the main navigation runs across the top of the page.
  • Container — Fluid (full-width, edge-to-edge content) or Boxed (max-width centered container with side margins for a more traditional feel).
  • Direction — Toggle between LTR (left-to-right) and RTL (right-to-left) text direction for multilingual support.
  • Reset — A single button that restores all settings to their defaults (Default Teal preset, Default density, Sidebar layout, Fluid container, LTR direction).

All preferences are persisted to localStorage, so they survive page refreshes and browser restarts. The next time the user visits the dashboard, their chosen color preset, density, layout, container, and direction settings are restored automatically.

RTL Support

The entire dashboard supports right-to-left text direction for languages such as Arabic, Hebrew, Persian, and Urdu. The toggle is available in the Theme Customizer under the Direction setting. When RTL is enabled, the sidebar moves to the right, text aligns to the right, and all directional spacing flips accordingly.

The implementation relies on the following techniques:

  • CSS Logical Properties — Instead of physical ml-*/ mr-* and pl-*/ pr-*, all spacing uses logical equivalents: ms-*/ me-*, ps-*/ pe-*, text-start/ text-end, border-s/ border-e, and rounded-s/ rounded-e. These automatically flip based on the document direction.
  • Tailwind CSS v4 ltr:/rtl: Variants — For cases where logical properties alone are not sufficient (e.g., icon rotation, absolute positioning, transform origins), Tailwind's ltr: and rtl: variants apply direction-specific styles conditionally.
  • dir attribute on HTML element — The Theme Customizer sets dir="rtl" on the <html> element, which browsers use natively to reverse text flow, scroll direction, and table column order.
  • Inline Script Prevents Flash — A small inline script in the <head> reads the saved direction preference from localStorage and applies the dir attribute before the page renders. This prevents a flash of incorrect direction (FOID) on page load, similar to how the theme script prevents a flash of incorrect theme.
<!-- Example: classes that work in both LTR and RTL -->
<div className="ms-4 me-2 ps-3 pe-3 text-start border-s-2">
  Content flows correctly in both directions
</div>

<!-- Example: direction-specific overrides -->
<ChevronRight className="size-4 ltr:rotate-0 rtl:rotate-180" />

Next Steps

See Components for a full list of available UI primitives, or learn how to add new pages to the dashboard.