Theming

Customize HeroUI's design system with CSS variables and global styles

Overview

HeroUI uses CSS variables and BEM classes for theming. You can customize everything from colors to component styles using standard CSS.

How It Works

HeroUI's theming system is built on top of Tailwind CSS v4's theme. When you import @heroui/styles, it:

  1. Uses Tailwind's built-in color palettes (like --color-neutral-*)
  2. Maps them to semantic variables for easier use
  3. Automatically switches between light and dark themes
  4. Uses CSS layers and the @theme directive for organization

The system follows a simple naming pattern:

  • Colors without a suffix are backgrounds (e.g., --accent)
  • Colors with -foreground are for text on that background (e.g., --accent-foreground)

Quick Start

Apply a Theme

Add a theme class to your HTML and apply colors to the body:

<html class="light" data-theme="light">
  <body class="bg-background text-foreground">
    <!-- Your app -->
  </body>
</html>

Switch Themes

<!-- Light theme -->
<html class="light" data-theme="light">

<!-- Dark theme -->
<html class="dark" data-theme="dark">

Override Colors

/* app/globals.css */
@import "tailwindcss";
@import "@heroui/styles";

:root {
  /* Override any color variable */
  --accent: oklch(0.7 0.25 260);
  --success: oklch(0.65 0.15 155);
}

Note: See Colors for the complete color palette and visual reference.

Create your own theme

/* src/themes/ocean.css */
@layer base {
  /* Ocean Light */
  [data-theme="ocean"] {
    color-scheme: light;

    /* Base */
    --background: oklch(0.985 0.015 225);
    --foreground: var(--eclipse);
    --panel: var(--white);
    --panel-foreground: var(--foreground);

    --radius: 0.75rem;

    --muted: var(--color-neutral-500);
    --scrollbar: var(--color-neutral-300);

    --default: var(--color-white);
    --default-foreground: var(--color-neutral-600);

    --surface-1: var(--background);
    --surface-2: oklch(0.965 0.020 225);
    --surface-3: oklch(0.930 0.030 225);

    /* Ocean accent */
    --accent: oklch(0.450 0.150 230);
    --accent-foreground: var(--snow);

    --accent-soft: oklch(0.920 0.040 230);
    --accent-soft-foreground: oklch(0.280 0.080 230);

    /* Status (kept compatible) */
    --success: oklch(0.5503 0.1244 153.56);
    --success-foreground: var(--snow);

    --warning: oklch(0.7186 0.1521 64.85);
    --warning-foreground: var(--eclipse);

    --danger: oklch(0.6259 0.1908 29.19);
    --danger-foreground: var(--snow);

    /* Component Colors */
    --segment: var(--white);
    --segment-foreground: var(--foreground);

    /* Misc */
    --border: oklch(0.50 0.060 230 / 22%);
    --divider: var(--color-neutral-200);
    --focus: oklch(0.60 0.150 230 / 35%);
    --link: var(--accent);

    --shadow-border:
      0 0px 5px 0px rgba(0, 10, 40, 0.03),
      0 2px 10px 0px rgba(0, 10, 40, 0.07),
      0 0px 1px 0px rgba(0, 0, 0, 0.25);
  }

  /* Ocean Dark */
  [data-theme="ocean-dark"] {
    color-scheme: dark;

    /* Base */
    --background: oklch(0.140 0.020 230);
    --foreground: var(--snow);
    --panel: oklch(0.180 0.020 230);

    --muted: var(--color-neutral-400);
    --scrollbar: var(--color-neutral-800);

    --default: var(--color-neutral-900);
    --default-foreground: var(--color-neutral-200);

    --surface-1: var(--background);
    --surface-2: oklch(0.220 0.030 230);
    --surface-3: oklch(0.280 0.030 230);

    /* Ocean accent */
    --accent: oklch(0.860 0.080 230);
    --accent-foreground: var(--eclipse);

    --accent-soft: oklch(0.300 0.050 230);
    --accent-soft-foreground: var(--color-neutral-200);

    /* Status */
    --success: oklch(80% 0.1561 154);
    --success-foreground: var(--eclipse);

    --warning: oklch(85% 0.1414 77.88);
    --warning-foreground: var(--eclipse);

    --danger: oklch(0.5798 0.1635 30.83);
    --danger-foreground: var(--snow);

    /* Component Colors */
    --segment: oklch(1 0 0 / 16%);
    --segment-foreground: var(--foreground);

    /* Misc */
    --border: oklch(1 0 0 / 12%);
    --divider: var(--color-neutral-800);
    --focus: oklch(0.860 0.080 230 / 40%);
    --link: var(--accent);

    --shadow-border:
      0 0 0 1px var(--border),
      0 5px 10px -3px rgba(0, 10, 40, 0.35),
      0 3px 4px -2px rgba(0, 0, 0, 0.12);
  }
}

Use your theme:

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

@import "tailwindcss";
@import "@heroui/styles";

@import "./src/themes/ocean.css" layer(theme); 

Apply your theme:

<!-- index.html -->

<!-- Light ocean -->
<html data-theme="ocean">

<!-- Dark ocean -->
<html data-theme="ocean-dark">

Customize Components

Global Component Styles

Override any component using BEM classes:

@layer components {
  /* Customize buttons */
  .button {
    @apply font-semibold tracking-wide;
  }

  .button--primary {
    @apply bg-blue-600 hover:bg-blue-700;
  }

  /* Customize accordions */
  .accordion__trigger {
    @apply text-lg font-bold;
  }
}

Note: See Styling for the complete styling reference.

Find Component Classes

Each component docs page lists all available classes:

  • Base classes: .button, .accordion
  • Modifiers: .button--primary, .button--icon-only
  • Elements: .accordion__trigger, .accordion__panel
  • States: [data-hover="true"], [aria-expanded="true"]

Example: Button classes

Import Strategies

Get everything with two lines:

@import "tailwindcss";
@import "@heroui/styles";

Selective Import

Import only what you need:

/* Define layers */
@layer theme, base, components, utilities;

/* Base requirements */
@import "tailwindcss";
@import "@heroui/styles/base/base.css" layer(base);
@import "@heroui/styles/themes/theme.css" layer(theme);
@import "@heroui/styles/themes/default.css" layer(theme);

/* Components (all components) */
@import "@heroui/styles/components/index.css" layer(components);
/* OR specific components */
@import "@heroui/styles/components/button.css" layer(components);
@import "@heroui/styles/components/accordion.css" layer(components);

Headless Mode

Build your own styles from scratch:

@import "tailwindcss";
@import "@heroui/styles/base/base.css";

/* Your custom styles */
.button {
  /* Your button styles */
}

Adding Custom Colors

You can add your own semantic colors to the theme:

/* Define in both light and dark themes */
:root, 
[data-theme="light"] {
  --info: oklch(0.6 0.15 210);
  --info-foreground: oklch(0.98 0 0);
}

.dark,
[data-theme="dark"] {
  --info: oklch(0.7 0.12 210);
  --info-foreground: oklch(0.15 0 0);
}

/* Make the color available to Tailwind */
@theme inline {
  --color-info: var(--info);
  --color-info-foreground: var(--info-foreground);
}

Now use it in your components:

<div className="bg-info text-info-foreground">Info message</div>

Variables Reference

HeroUI defines three types of variables:

  1. Base Variables: Non-changing values like --white, --black, spacing, and typography
  2. Theme Variables: Colors that change between light/dark themes
  3. Calculated Variables: Automatically generated hover states and size variants

For a complete reference of all variables and their values, see:

Calculated Variables (Tailwind)

We use Tailwind's @theme directive to automatically create calculated variables for hover states and radius variants:

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-panel: var(--panel);
--color-panel-foreground: var(--panel-foreground);

--color-muted: var(--muted);

--color-surface: var(--surface-1);
--color-surface-foreground: var(--foreground);

--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);

--color-accent-soft: var(--accent-soft);
--color-accent-soft-foreground: var(--accent-soft-foreground);

--color-segment: var(--segment);
--color-segment-foreground: var(--segment-foreground);

--color-border: var(--border);
--color-divider: var(--divider);
--color-focus: var(--focus);
--color-link: var(--link);

--color-default: var(--default);
--color-default-foreground: var(--default-foreground);

--color-success: var(--success);
--color-success-foreground: var(--success-foreground);

--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);

--color-danger: var(--danger);
--color-danger-foreground: var(--danger-foreground);

--shadow-border: var(--shadow-border);

/* Calculated Variables */

/* Colors */
--color-panel-hover: color-mix(
  in oklab,
  var(--color-panel) 90%,
  var(--color-panel-foreground) 5%
);
--color-default-hover: color-mix(
  in oklab,
  var(--color-default) 90%,
  var(--color-default-foreground) 5%
);
--color-accent-hover: color-mix(
  in oklab,
  var(--color-accent) 90%,
  var(--color-accent-foreground) 20%
);
--color-accent-soft-hover: color-mix(
  in oklab,
  var(--color-accent-soft) 10%,
  var(--color-accent-soft-foreground) 18%
);
--color-success-hover: color-mix(
  in oklab,
  var(--color-success) 90%,
  var(--color-success-foreground) 10%
);
--color-warning-hover: color-mix(
  in oklab,
  var(--color-warning) 90%,
  var(--color-warning-foreground) 10%
);
--color-danger-hover: color-mix(
  in oklab,
  var(--color-danger) 90%,
  var(--color-danger-foreground) 10%
);

/* Surface Colors */
--color-surface-1: var(--surface-1);
--color-surface-1-foreground: color-mix(
  in oklab,
  var(--color-surface-1) 80%,
  var(--color-foreground) 3.5%
);
--color-surface-2: var(--surface-2);
--color-surface-2-foreground: color-mix(
  in oklab,
  var(--color-surface-2) 80%,
  var(--color-foreground) 3.5%
);
--color-surface-3: var(--surface-3);
--color-surface-3-foreground: color-mix(
  in oklab,
  var(--color-surface-3) 80%,
  var(--color-foreground) 3.5%
);

/* Radius */
--radius-xs: calc(var(--radius) * 0.25);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: calc(var(--radius) * 0.75);
--radius-lg: calc(var(--radius) * 1);
--radius-xl: calc(var(--radius) * 1.5);
--radius-2xl: calc(var(--radius) * 2);
--radius-3xl: calc(var(--radius) * 3);
--radius-4xl: calc(var(--radius) * 4);
--radius-panel: var(--radius);
--radius-panel-inner: calc(var(--radius) * 0.5);

/* Transition Timing Functions  */
--ease-smooth: ease; /* same as transition: ease; */

/* From smoother to faster quad->circ @made by https://twitter.com/bdc */
--ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
--ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
--ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22);
--ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
--ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335);

--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
--ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
--ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1);

--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
--ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1);
--ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
--ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86);

/* Animations */
--animate-spin-fast: spin 0.75s linear infinite;
--animate-skeleton: skeleton 2s linear infinite;
}

Resources