---
title: Theming
description: Customize HeroUI's design system with CSS variables and global styles
icon: palette
links:
  themes: true
  tailwind: theme
---

HeroUI uses CSS variables and [BEM](https://getbem.com/) classes for theming. Customize everything from colors to component styles using standard CSS.

<Callout type="info">
  **Want to create your own theme?** Try the [Theme Builder](/themes) to visually customize colors, radius, fonts, and more — then export the CSS to use in your project.
</Callout>

## How It Works

HeroUI's theming system is built on top of [Tailwind CSS v4](https://tailwindcss.com/docs/theme)'s theme. When you import `@heroui/styles`, it uses Tailwind's built-in color palettes, maps them to semantic variables, automatically switches between light and dark themes, and uses CSS layers and the `@theme` directive for organization.

**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
<html class="light" data-theme="light">
  <body class="bg-background text-foreground">
    <!-- Your app -->
  </body>
</html>
```

**Switch themes:**
```html
<!-- Light theme -->
<html class="light" data-theme="light">

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

**Switch themes programmatically with [next-themes](https://github.com/pacocoursey/next-themes) (For Next.js):**

First, wrap your app with `ThemeProvider`:

```tsx
// app/providers.tsx
"use client";

import { ThemeProvider } from "next-themes";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="light">
      {children}
    </ThemeProvider>
  );
}
```

```tsx
// app/layout.tsx
import { Providers } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className="bg-background text-foreground">
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

Then use `useTheme` to toggle between themes:

```tsx
"use client";

import { useTheme } from "next-themes";

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

  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Toggle {theme === "dark" ? "Light" : "Dark"} Mode
    </button>
  );
}
```

**Override colors:**

```css
/* 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](/docs/handbook/colors) for the complete color palette and visual reference.

> **Dark mode**: For a complete setup guide with `next-themes` and HeroUI's `useTheme` hook, see [Dark Mode](/docs/handbook/dark-mode).

**Create your own theme:**

<CollapsibleCode lang="css" code={`/* 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: 1px;
      --field-border-width: 0px;
      --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-thumb: color-mix(in oklch, var(--foreground) 15%, transparent);
      --scrollbar-track: transparent;
      --scrollbar-gutter: auto;
      --scrollbar-width: thin;
      --scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
      --scrollbar: var(--scrollbar-thumb);

      --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%);
      --separator: 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 */

      /* Tooltip Default Delays */
      --tooltip-delay: 1500ms;
      --tooltip-close-delay: 500ms;
    }

    /* 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-thumb: color-mix(in oklch, var(--foreground) 15%, transparent);
      --scrollbar-track: transparent;
      --scrollbar-gutter: auto;
      --scrollbar-width: thin;
      --scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
      --scrollbar: var(--scrollbar-thumb);

      --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(22% 0.006 286.033);
      --separator: 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:

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

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

@import "./src/themes/ocean.css" layer(theme); /* [!code highlight]*/
```

Apply your theme:

```html
<!-- 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:

```css
@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](/docs/handbook/styling) for the complete styling reference.

**Find component classes:** Each component docs page lists all available classes (base classes, modifiers, elements, states). Example: [Button classes](/docs/components/button#css-classes)

## Import Strategies

**Full import (recommended):** Get everything with two lines:

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

**Selective import:** Import only what you need:

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

/* Base requirements */
@import "tailwindcss";
@import "@heroui/styles/base" layer(base);
/* OR specific base file */
@import "@heroui/styles/base/base.css" layer(base);

/* Theme variables */
@import "@heroui/styles/themes/shared/theme.css" layer(theme);
@import "@heroui/styles/themes/default" layer(theme);
/* OR specific theme files */
@import "@heroui/styles/themes/default/index.css" layer(theme);
@import "@heroui/styles/themes/default/variables.css" layer(theme);

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

/* Utilities (optional) */
@import "@heroui/styles/utilities" layer(utilities);

/* Variants (optional) */
@import "@heroui/styles/variants" layer(utilities);
```

> **Note**: Directory imports (e.g., `@heroui/styles/components`) automatically resolve to their `index.css` file. Use explicit file paths (e.g., `@heroui/styles/components/button.css`) to import individual component styles.

**Headless mode:** Build your own styles from scratch:

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

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

## Adding Custom Colors

Add your own semantic colors to the theme:

```css
/* 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:

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

## Variables Reference

HeroUI defines three types of variables in [`variables.css`](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/variables.css):

1. **Base Variables** — Non-changing values like `--white`, `--black`, spacing, and typography
2. **Theme Variables** — Colors that change between light/dark themes, plus scrollbar tokens (`--scrollbar-thumb`, `--scrollbar-width`, etc.)
3. **Calculated Variables** — Hover states, soft variants, and border/separator levels (the **Calculated Colors** block in each light/dark theme, using `color-mix()`)

For a complete reference, see: [Colors Documentation](/docs/handbook/colors), [Default Theme Variables](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/variables.css), [Shared Theme Utilities](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/shared/theme.css)

**Tailwind theme bridge (`@theme inline`):**

[`themes/shared/theme.css`](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/shared/theme.css) maps semantic variables to Tailwind tokens (`--color-*`, `--radius-*`, `--ease-*`). Calculated colors reference underlying vars (e.g. `--surface-hover`, `--accent-soft`) from `variables.css` — they are not inlined with `color-mix()` in this file:
<CollapsibleCode lang="css" code={`  @theme inline {
    --color-background: var(--background);
    --color-foreground: var(--foreground);

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

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

    --color-surface-tertiary: var(--surface-tertiary);
    --color-surface-tertiary-foreground: var(--surface-tertiary-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-separator: var(--separator);
    --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);

    --color-backdrop: var(--backdrop);

    --shadow-surface: var(--surface-shadow);
    --shadow-overlay: var(--overlay-shadow);
    --shadow-field: var(--field-shadow);

    /* Form Field Tokens */
    --color-field: var(--field-background, var(--default));
    --color-field-hover: var(--field-hover);
    --color-field-foreground: var(--field-foreground, var(--foreground));
    --color-field-placeholder: var(--field-placeholder, var(--muted));
    --color-field-border: var(--field-border, var(--border));
    --radius-field: var(--field-radius, calc(var(--radius) * 1.5));
    --border-width-field: var(--field-border-width, var(--border-width));

    /* Color Tokens */
    --color-background-secondary: var(--background-secondary);
    --color-background-tertiary: var(--background-tertiary);
    --color-background-inverse: var(--background-inverse);

    --color-default-hover: var(--default-hover);
    --color-accent-hover: var(--accent-hover);
    --color-success-hover: var(--success-hover);
    --color-warning-hover: var(--warning-hover);
    --color-danger-hover: var(--danger-hover);

    /* Form Field Colors */
    --color-field-focus: var(--field-focus);
    --color-field-border-hover: var(--field-border-hover);
    --color-field-border-focus: var(--field-border-focus);

    /* Soft Colors */
    --color-default-soft: var(--default-soft);
    --color-default-soft-foreground: var(--default-soft-foreground);
    --color-default-soft-hover: var(--default-soft-hover);

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

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

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

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

    /* Separator Colors - Levels */
    --color-separator-secondary: var(--separator-secondary);
    --color-separator-tertiary: var(--separator-tertiary);

    /* Border Colors - Levels */
    --color-border-secondary: var(--border-secondary);
    --color-border-tertiary: var(--border-tertiary);

    /* Radius and default sizes - defaults can change by just changing the --radius */
    --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; /* same as transition: ease; */
    /* These custom curves are made by https://twitter.com/bdc */

    /* From smoother to faster */
    --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);
    /* From slower to faster */
    --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);
    /* Custom smooth-out curve: fast start, smooth stop - Apple style */
    --ease-out-fluid: cubic-bezier(0.32, 0.72, 0, 1);
    /* From slower to faster */
    --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);

    /* Linear */
    --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 rely on `--field-*` theme variables. Hover, focus, and border variants are defined under **Calculated Colors** in `variables.css` and mapped to Tailwind in `theme.css` (e.g. `--color-field-hover: var(--field-hover)`). Override `--field-background`, `--field-hover`, and related tokens in your theme to restyle inputs, checkboxes, radios, and OTP slots without affecting surfaces like buttons or cards.

## Scrollbars

HeroUI applies a shared scrollbar style to component scroll areas (tables, popovers, drawers, and similar). Scrollbars use standard CSS properties (`scrollbar-width`, `scrollbar-color`, `scrollbar-gutter`) — no `::-webkit-scrollbar` overrides.

**Modes** — set `data-scrollbar` on `<html>`, a component root, or a scroll slot:

| Mode | `data-scrollbar` | Behavior |
|------|------------------|----------|
| HeroUI thin | *(unset)* or `thin` | Thin thumb from theme tokens |
| OS / browser | `default` | Native scrollbars (`auto`) |
| Hidden | `none` | No visible scrollbar (`scrollbar-width: none`) |

```html
<!-- Native scrollbars everywhere -->
<html data-scrollbar="default">

<!-- Restore HeroUI scrollbars in one subtree -->
<main data-scrollbar="thin">
  ...
</main>

<!-- Hide scrollbars in one subtree (e.g. a popover) -->
<div data-scrollbar="none">
  ...
</div>
```

**Theme variables** — defined in light and dark theme blocks in [`variables.css`](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/variables.css):

| Variable | Description |
|----------|-------------|
| `--scrollbar-thumb` | Thumb color (default: 15% `--foreground` via `color-mix`) |
| `--scrollbar-track` | Track color (default: `transparent`) |
| `--scrollbar-gutter` | Gutter (default: `auto`) |
| `--scrollbar-width` | `scrollbar-width` (default: `thin`) |
| `--scrollbar-color` | `scrollbar-color` (default: thumb + track) |
| `--scrollbar` | Legacy alias of `--scrollbar-thumb` |

**Customize globally:**

```css
/* app/globals.css */
:root {
  --scrollbar-thumb: color-mix(in oklch, var(--accent) 30%, transparent);
  --scrollbar-gutter: auto;
}
```

**Per scroll slot** — pass `data-scrollbar` on a component or override tokens on a wrapper:

```tsx
<Table data-scrollbar="default">
  ...
</Table>

<Drawer.Content data-scrollbar="thin" />

<Calendar.YearPickerGrid data-scrollbar="none" />
```

**Custom overflow areas** — use the `scrollbar`, `scrollbar-thin`, `scrollbar-default`, or `scrollbar-none` utilities from `@heroui/styles` on your own elements. See [Styling](/docs/handbook/styling) for class-based overrides.

> **Note**: Some components hide scrollbars by default (date picker popovers, color picker, secondary tabs). Nested scroll slots (e.g. the calendar year picker inside a date picker) keep HeroUI scrollbars because `scrollbar-none` only affects the element it is applied to, not descendants using `@apply scrollbar`.

## Vibrant Palette

By default, HeroUI uses accessible soft foreground colors that mix the semantic color with the foreground for better contrast. If you prefer more saturated, vibrant soft foreground colors, add the `data-vibrant-palette` attribute to your root element:

```html
<html data-vibrant-palette="true">
```

This switches all `*-soft-foreground` variables (accent, success, warning, danger) to use 92% of the semantic color with only 8% foreground mixed in — closer to the raw color but with a slight contrast boost.

| Mode | Accessible (default) | Vibrant |
|------|----------------------|---------|
| Soft foreground | `color-mix(color 70-80%, foreground 30-40%)` | `color-mix(color 92%, foreground 8%)` |

<Callout type="warn">
  The vibrant palette prioritizes visual saturation over contrast. It may not meet WCAG accessibility guidelines for some color combinations, especially with lighter accent colors.
</Callout>

You can also enable vibrant palette in the [Theme Builder](/themes) via the "Vibrant palette" toggle in the theme popover.

## Resources

- [Colors Documentation](/docs/handbook/colors)
- [Styling Guide](/docs/handbook/styling)
- [Tailwind CSS v4 Theming](https://tailwindcss.com/docs/theme)
- [BEM Methodology](https://getbem.com/)
- [OKLCH Color Tool](https://oklch.com)
