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;

    /* Primitive Colors (Do not change between light and dark) */
    --white: oklch(100% 0 0);
    --black: oklch(0% 0 0);
    --snow: oklch(0.9911 0 0);
    --eclipse: oklch(0.2103 0.0059 285.89);

    /* Spacing & Layout */
    --spacing: 0.25rem;
    --border-width: 0px;
    --field-border-width: var(--border-width);
    --disabled-opacity: 0.5;
    --ring-offset-width: 2px;
    --cursor-interactive: pointer;
    --cursor-disabled: not-allowed;

    /* Radius */
    --radius: 0.75rem;
    --field-radius: calc(var(--radius) * 1.5);

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

    /* Surface: Used for non-overlay components */
    --surface: var(--white);
    --surface-foreground: var(--foreground);

    /* Overlay: Used for floating/overlay components */
    --overlay: var(--white);
    --overlay-foreground: var(--foreground);

    --muted: oklch(0.5517 0.0138 285.94);
    --scrollbar: oklch(87.1% 0.006 286.286);

    --default: oklch(94% 0.001 286.375);
    --default-foreground: var(--eclipse);

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

    /* Form Field Defaults */
    --field-background: var(--white);
    --field-foreground: oklch(0.2103 0.0059 285.89);
    --field-placeholder: var(--muted);
    --field-border: transparent;

    /* Status (kept compatible) */
    --success: oklch(0.7329 0.1935 150.81);
    --success-foreground: var(--eclipse);

    --warning: oklch(0.7819 0.1585 72.33);
    --warning-foreground: var(--eclipse);

    --danger: oklch(0.6532 0.2328 25.74);
    --danger-foreground: var(--snow);

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

    /* Misc */
    --border: oklch(0.50 0.060 230 / 22%);
    --divider: oklch(92% 0.004 286.32);
    --focus: var(--accent);
    --link: var(--accent);

    /* Shadows */
    --surface-shadow:
      0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
      0 0 1px 0 rgba(0, 0, 0, 0.06);
    --overlay-shadow: 0 4px 16px 0 rgba(24, 24, 27, 0.08), 0 8px 24px 0 rgba(24, 24, 27, 0.09);
    --field-shadow:
      0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
      0 0 1px 0 rgba(0, 0, 0, 0.06);

    /* Skeleton Default Global Animation */
    --skeleton-animation: shimmer; /* Possible values: shimmer, pulse, none */
  }

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

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

    /* Surface: Used for non-overlay components */
    --surface: oklch(0.2103 0.0059 285.89);
    --surface-foreground: var(--foreground);

    /* Overlay: Used for floating/overlay components */
    --overlay: oklch(0.22 0.0059 285.89);
    --overlay-foreground: var(--foreground);

    --muted: oklch(70.5% 0.015 286.067);
    --scrollbar: oklch(70.5% 0.015 286.067);

    --default: oklch(27.4% 0.006 286.033);
    --default-foreground: var(--snow);

    /* Form Field Defaults */
    --field-background: var(--default);
    --field-foreground: var(--foreground);

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

    /* Status */
    --success: oklch(0.7329 0.1935 150.81);
    --success-foreground: var(--eclipse);

    --warning: oklch(0.8203 0.1388 76.34);
    --warning-foreground: var(--eclipse);

    --danger: oklch(0.594 0.1967 24.63);
    --danger-foreground: var(--snow);

    /* Component Colors */
    --segment: oklch(0.3964 0.01 285.93);
    --segment-foreground: var(--foreground);

    /* Misc */
    --border: oklch(1 0 0 / 0%);
    --divider: oklch(22% 0.006 286.033);
    --focus: var(--accent);
    --link: var(--accent);

    /* Shadows */
    --surface-shadow: 0 0 0 0 transparent inset;
    --overlay-shadow: 0 0 0 0 transparent inset;
    --field-shadow: 0 0 0 0 transparent inset;
  }
}

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-hovered="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/shared/theme.css" layer(theme);
@import "@heroui/styles/themes/default" 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. These are defined in themes/shared/theme.css:

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

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

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

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

  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-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-surface: var(--surface-shadow);
  --shadow-overlay: var(--overlay-shadow);
  --shadow-field: var(--field-shadow);

  /* Form Field Tokens */
  --color-field: var(--field-background, var(--color-default));
  --color-field-hover: color-mix(
    in oklab,
    var(--field-background, var(--color-default)) 90%,
    var(--field-foreground, var(--color-default-foreground)) 10%
  );
  --color-field-foreground: var(--field-foreground, var(--color-foreground));
  --color-field-placeholder: var(--field-placeholder, var(--color-muted));
  --color-field-border: var(--field-border, var(--color-border));
  --radius-field: var(--field-radius, var(--radius-xl));
  --border-width-field: var(--field-border-width, var(--border-width));

  /* Calculated Variables */

  /* --- background shades --- */
  --color-background-secondary: color-mix(
    in oklab,
    var(--color-background) 96%,
    var(--color-foreground) 4%
  );
  --color-background-tertiary: color-mix(
    in oklab,
    var(--color-background) 92%,
    var(--color-foreground) 8%
  );
  --color-background-quaternary: color-mix(
    in oklab,
    var(--color-background) 86%,
    var(--color-foreground) 14%
  );
  --color-background-inverse: var(--color-foreground);

  /* Hover states */
  --color-default-hover: color-mix(in oklab, var(--color-default) 80%, transparent);
  --color-accent-hover: color-mix(
    in oklab,
    var(--color-accent) 90%,
    var(--color-accent-foreground) 10%
  );
  --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%
  );

  /* Form Field Colors */
  --color-field-hover: color-mix(
    in oklab,
    var(--color-field) 90%,
    var(--color-field-foreground) 2%
  );
  --color-field-focus: var(--color-field);
  --color-field-border-hover: color-mix(
    in oklab,
    var(--color-field-border) 88%,
    var(--color-field-foreground) 10%
  );
  --color-field-border-focus: color-mix(
    in oklab,
    var(--color-field-border) 74%,
    var(--color-field-foreground) 22%
  );

  /* Soft Colors */
  --color-accent-soft: color-mix(in oklab, var(--color-accent) 15%, transparent);
  --color-accent-soft-foreground: var(--color-accent);
  --color-accent-soft-hover: color-mix(in oklab, var(--color-accent) 20%, transparent);

  --color-danger-soft: color-mix(in oklab, var(--color-danger) 15%, transparent);
  --color-danger-soft-foreground: var(--color-danger);
  --color-danger-soft-hover: color-mix(in oklab, var(--color-danger) 20%, transparent);

  --color-warning-soft: color-mix(in oklab, var(--color-warning) 15%, transparent);
  --color-warning-soft-foreground: var(--color-warning);
  --color-warning-soft-hover: color-mix(in oklab, var(--color-warning) 20%, transparent);

  --color-success-soft: color-mix(in oklab, var(--color-success) 15%, transparent);
  --color-success-soft-foreground: var(--color-success);
  --color-success-soft-hover: color-mix(in oklab, var(--color-success) 20%, transparent);

  /* Surface Levels */
  --color-surface-secondary: color-mix(in oklab, var(--surface) 94%, var(--surface-foreground) 6%);
  --color-surface-tertiary: color-mix(in oklab, var(--surface) 92%, var(--surface-foreground) 8%);
  --color-surface-quaternary: color-mix(
    in oklab,
    var(--surface) 86%,
    var(--default-foreground) 14%
  );

  /* On Surface Colors */
  --color-on-surface: color-mix(
    in oklab,
    var(--color-surface) 93%,
    var(--color-surface-foreground) 7%
  );
  --color-on-surface-foreground: var(--color-surface-foreground);
  --color-on-surface-hover: color-mix(
    in oklab,
    var(--color-surface) 91%,
    var(--color-surface-foreground) 9%
  );
  --color-on-surface-focus: var(--color-on-surface);

  /* Radius scale */
  --radius-xs: calc(var(--radius) * 0.25); /* 0.125rem (2px) */
  --radius-sm: calc(var(--radius) * 0.5); /* 0.25rem (4px) */
  --radius-md: calc(var(--radius) * 0.75); /* 0.375rem (6px) */
  --radius-lg: calc(var(--radius) * 1); /* 0.5rem (8px) */
  --radius-xl: calc(var(--radius) * 1.5); /* 0.75rem (12px) */
  --radius-2xl: calc(var(--radius) * 2); /* 1rem (16px) */
  --radius-3xl: calc(var(--radius) * 3); /* 1.5rem (24px) */
  --radius-4xl: calc(var(--radius) * 4); /* 2rem (32px) */

  /* Transition Timing Functions  */
  --ease-smooth: ease;
  --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-fluid-out: cubic-bezier(0.32, 0.72, 0, 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);
  --ease-linear: linear;

  /* Animations */
  --animate-spin-fast: spin 0.75s linear infinite;
  --animate-skeleton: skeleton 2s linear infinite;
  --animate-caret-blink: caret-blink 1.2s ease-out infinite;

  @keyframes skeleton {
    100% {
      transform: translateX(200%);
    }
  }

  @keyframes caret-blink {
    0%,
    70%,
    100% {
      opacity: 1;
    }
    20%,
    50% {
      opacity: 0;
    }
  }
}

Form controls now rely on the --field-* variables and their calculated hover/focus variants. Update them in your theme to restyle inputs, checkboxes, radios, and OTP slots without impacting surfaces like buttons or cards.

Resources