Theming

Customize HareUI's design system with CSS variables and app-level theme overrides.

HareUI uses CSS variables for color/spacing tokens and tailwind-variants slot themes for component styles. Customize everything from colors to individual component slots with standard CSS or an app-level config object — no BEM classes required.

How It Works

HareUI's theming system is built on Tailwind CSS v4. The @hareui/vue stylesheet declares semantic CSS variables (--accent, --surface, --radius, …) for light and dark themes, maps them to Tailwind's @theme inline tokens, and each component's .vue file consumes them through a tailwind-variants slot object.

Naming pattern:

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

Two Ways to Customize

LayerScopeMechanism
CSS variablesGlobal, affects every component that reads the tokenOverride in :root / .dark
createHareUI({ ui })Global, affects one component's slot classes/variantsApp-level Vue plugin config
:ui propSingle instanceProp on the component
class propSingle instance, root slot onlyProp on the component

Overriding CSS Variables

Add overrides after the @hareui/vue import:

/* src/style.css */
@import "tailwindcss";
@import "@hareui/vue";

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

.dark {
  --accent: oklch(0.8 0.18 260);
}

See Colors for the complete token reference and Dark Mode for the .dark / data-theme="dark" switch.

Overriding Components with createHareUI

Pass a ui config to createHareUI() to override a component's slot classes, variant classes, or default variants app-wide. Keys match the component's theme name (button, card, checkbox, …); values follow the same slots / variants / defaultVariants shape as the component's internal theme file.

// main.ts
import { createApp } from 'vue'
import { createHareUI } from '@hareui/vue'
import App from './App.vue'

createApp(App)
  .use(createHareUI({
    ui: {
      button: {
        // Override the base slot class
        slots: { base: 'uppercase tracking-wider' },
        // Override one variant's class
        variants: { variant: { primary: 'shadow-lg' } },
        // Change the default variant
        defaultVariants: { size: 'lg' },
      },
    },
  }))
  .mount('#app')

Every component's slot keys are documented on its own page's Anatomy section (e.g. Button, Card).

Overriding a Single Instance

Use the :ui prop for per-slot overrides, or class for the root slot, on one instance:

<template>
  <!-- Root-only override, tailwind-merged into the base slot -->
  <Button class="rounded-none">Square corners</Button>

  <!-- Per-slot override -->
  <Button :ui="{ base: 'uppercase', spinner: 'text-white' }">
    Shout
  </Button>
</template>

Precedence

When the same slot is targeted at multiple layers, HareUI resolves conflicts with tailwind-merge — the last applied class wins. Layers are applied in this order, lowest to highest priority:

  1. Theme file — the component's built-in default (src/theme/<name>.ts)
  2. createHareUI({ ui }) — app-level config
  3. Variant props — variant, size, color, …
  4. :ui slot classes — per-instance, per-slot
  5. class prop — per-instance, root slot only, highest priority
<script setup lang="ts">
// Theme default: 'px-4'. App config sets 'px-1'. :ui sets 'px-2'. class sets 'px-3'.
// Final root class: 'px-3' — class wins over everything else via tailwind-merge.
</script>

<template>
  <Button :ui="{ base: 'px-2' }" class="px-3">Click me</Button>
</template>

Adding Custom Colors

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:

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

Variables Reference

HareUI defines three types of variables in packages/vue/src/styles/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 (derived with color-mix())

For a complete reference, see Colors and packages/vue/src/styles/variables.css.

Tailwind theme bridge (@theme inline):

packages/vue/src/styles/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:

theme.css
@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 calculated 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 Input, TextField, Checkbox, Switch, and RadioGroup without affecting surfaces like Card or Button.

Scrollbars

HareUI applies a shared scrollbar style to component scroll areas (currently Modal's body). 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:

Modedata-scrollbarBehavior
HareUI thin(unset) or thinThin thumb from theme tokens
OS / browserdefaultNative scrollbars (auto)
HiddennoneNo visible scrollbar (scrollbar-width: none)
<!-- Native scrollbars everywhere -->
<html data-scrollbar="default">

<!-- Hide scrollbars in one subtree -->
<div data-scrollbar="none">
  ...
</div>

Theme variables — defined in the light and dark theme blocks in variables.css:

VariableDescription
--scrollbar-thumbThumb color (default: 15% --foreground via color-mix)
--scrollbar-trackTrack color (default: transparent)
--scrollbar-gutterGutter (default: auto)
--scrollbar-widthscrollbar-width (default: thin)
--scrollbar-colorscrollbar-color (default: thumb + track)
--scrollbarLegacy alias of --scrollbar-thumb

Customize globally:

/* src/style.css */
:root {
  --scrollbar-thumb: color-mix(in oklch, var(--accent) 30%, transparent);
}

Custom overflow areas — use the scrollbar, scrollbar-thin, scrollbar-default, or scrollbar-none utility classes on your own elements:

<template>
  <div class="scrollbar h-64 overflow-y-auto">
    <!-- long content -->
  </div>
</template>

Vibrant Palette

By default, HareUI 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 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.

ModeAccessible (default)Vibrant
Soft foregroundcolor-mix(color 70-80%, foreground 30-40%)color-mix(color 92%, foreground 8%)

The vibrant palette prioritizes visual saturation over contrast. It may not meet WCAG accessibility guidelines for some color combinations, especially with lighter accent colors.

Resources

On this page

No Headings