Styling

Style HareUI components with the class prop, the ui prop, and data attribute selectors.

Every HareUI component accepts a class prop for the root element and a :ui prop for per-slot overrides. Both merge on top of the component's built-in theme with tailwind-merge, so conflicting utilities resolve predictably instead of stacking.

The class Prop

class targets the root slot (always named base internally) and wins over everything below it in the precedence order:

<template>
  <Button class="bg-purple-500 hover:bg-purple-600">
    Custom Button
  </Button>
</template>

Because HareUI uses tailwind-merge, bg-purple-500 replaces the theme's bg-(--button-bg) instead of both classes competing at the same specificity.

The :ui Prop

:ui targets any slot by name, not just the root. Each component documents its slot keys on its own page's Anatomy section:

<template>
  <Button :ui="{ base: 'uppercase tracking-wider', spinner: 'text-white' }" :pending="true">
    Submit
  </Button>
</template>

Scrollbars

HareUI's scroll slots (e.g. Modal's body) apply the scrollbar utility, which reads the theme's --scrollbar-* variables. Use the same utilities from @hareui/vue on your own overflow containers:

<template>
  <div class="scrollbar h-64 overflow-y-auto">
    <!-- long content -->
  </div>
</template>
UtilityEffect
scrollbarHareUI thumb (reads theme --scrollbar-* variables)
scrollbar-thinHareUI themed thin scrollbar
scrollbar-defaultOS / browser scrollbars
scrollbar-noneHidden scrollbar

Global and per-subtree control uses data-scrollbar on an ancestor. See Theming for tokens and modes.

Tailwind-Merge Conflict Resolution

Classes are combined in this order — later layers win when they target the same CSS property: theme file → createHareUI({ ui }) → variant props → :ui → class (root only). See Theming for the full precedence table.

<!-- Theme default is rounded-3xl; class overrides it to rounded-none -->
<Button class="rounded-none">Square corners</Button>

State-Based Styling

HareUI components expose their state through data-slot and state data attributes, mirroring HeroUI's selectors:

/* Target component state */
[data-slot="button"][data-pending="true"] {
  cursor: progress;
}

[data-slot="checkbox-control"][data-state="checked"] {
  background: var(--accent);
}

[data-slot="modal-dialog"][data-state="open"] {
  animation: fade-in 150ms var(--ease-out);
}

Common attributes:

AttributeMeaning
data-slot="<name>"Identifies the rendered part, identical to HeroUI's data-slot strings
data-state="checked|open|closed|active"Reka UI state (checkbox/switch/radio, overlays, tabs)
data-disabledPresent when the part is disabled
data-pending="true"Button is in a pending state
data-invalid="true"Form field failed validation
data-side="top|right|bottom|left"Floating content placement (popover, tooltip)

See Animation for the full data-state → Tailwind variant mapping used in transitions.

Native :hover/:active/:focus-visible

HareUI components are plain DOM elements, so native pseudo-classes work directly — no data-hovered/data-pressed attributes to match:

[data-slot="button"]:hover {
  background: var(--accent-hover);
}

[data-slot="button"]:active {
  transform: scale(0.97);
}

[data-slot="button"]:focus-visible {
  outline: 2px solid var(--focus);
}

Extending Component Themes

Every component's theme config is exported from @hareui/vue's themes namespace (themes.button, themes.card, …) in the same shape tv() expects. Extend one to add a variant that doesn't exist in the original component, resolve it with tv(), and apply the resolved class through the class prop:

<script setup lang="ts">import { Button, themes, tv } from '@hareui/vue'const myButtonVariants = tv({  extend: tv(themes.button),  slots: {    base: 'font-semibold shadow-md text-shadow-lg data-[pending=true]:opacity-40',  },  variants: {    radius: {      full: { base: 'rounded-full' },      lg: { base: 'rounded-lg' },      md: { base: 'rounded-md' },      sm: { base: 'rounded-sm' },    },    size: {      sm: { base: 'h-10 px-4' },      md: { base: 'h-11 px-6' },      lg: { base: 'h-12 px-8' },      xl: { base: 'h-13 px-10' },    },    variant: {      primary: { base: 'text-white dark:bg-white/10 dark:text-white dark:hover:bg-white/15' },    },  },  defaultVariants: {    radius: 'full',    variant: 'primary',  },})</script><template>  <Button :class="myButtonVariants({ radius: 'full', variant: 'primary' }).base()">    Custom Button  </Button></template>

Because tv({ extend }) merges with tailwind-merge, the new variant's classes win over the base theme's conflicting utilities without touching the original button theme file.

Responsive Design

class and :ui accept any Tailwind utility, including responsive variants:

<template>
  <Button class="text-sm md:text-base lg:text-lg px-3 md:px-4 lg:px-6">
    Responsive Button
  </Button>
</template>

CSS Modules

Vue's <style module> works the same way as any other class value — bind the generated identifier instead of a literal string:

<!-- GradientButton.vue -->
<script setup lang="ts">
import { Button } from '@hareui/vue'
</script>

<template>
  <Button :class="$style.button">Scoped Button</Button>
</template>

<style module>
.button {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
}
</style>

Next Steps

On this page

No Headings