Button
A clickable button component with multiple variants and states
Usage
import { Button } from '@hareui/vue'<script setup lang="ts">import { Button } from '@hareui/vue'</script><template> <Button>Click me</Button></template>Anatomy
Button is a single component — no compound parts. Content is composed through named slots instead of HeroUI's render-prop children.
<Button>
<template #leading />
Label
<template #trailing />
</Button>ui keys: base, spinner.
Examples
Variants
<script setup lang="ts">import { Button } from '@hareui/vue'</script><template> <div class="flex flex-wrap gap-3"> <Button>Primary</Button> <Button variant="secondary">Secondary</Button> <Button variant="tertiary">Tertiary</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="danger">Danger</Button> <Button variant="danger-soft">Danger Soft</Button> </div></template>Sizes
<script setup lang="ts">import { Button } from '@hareui/vue'</script><template> <div class="flex items-center gap-3"> <Button size="sm">Small</Button> <Button size="md">Medium</Button> <Button size="lg">Large</Button> </div></template>With Icons
<script setup lang="ts">import { Button } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <div class="flex flex-wrap gap-3"> <Button> <Icon icon="gravity-ui:globe" /> Search </Button> <Button variant="secondary"> <Icon icon="gravity-ui:plus" /> Add Member </Button> <Button variant="tertiary"> <Icon icon="gravity-ui:envelope" /> Email </Button> <Button variant="danger"> <Icon icon="gravity-ui:trash-bin" /> Delete </Button> </div></template>Icon Only
<script setup lang="ts">import { Button } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <div class="flex gap-3"> <Button aria-label="More options" icon-only variant="tertiary"> <Icon icon="gravity-ui:ellipsis" /> </Button> <Button aria-label="Settings" icon-only variant="secondary"> <Icon icon="gravity-ui:gear" /> </Button> <Button aria-label="Delete" icon-only variant="danger"> <Icon icon="gravity-ui:trash-bin" /> </Button> </div></template>Loading
<script setup lang="ts">import { Button } from '@hareui/vue'</script><template> <Button pending>Uploading...</Button></template>Loading State
<script setup lang="ts">import { ref } from 'vue'import { Button } from '@hareui/vue'import { Icon } from '@iconify/vue'const isPending = ref(false)function onClick() { isPending.value = true setTimeout(() => (isPending.value = false), 2000)}</script><template> <Button :pending="isPending" @click="onClick"> <template v-if="!isPending" #leading> <Icon icon="gravity-ui:paperclip" /> </template> {{ isPending ? 'Uploading...' : 'Upload File' }} </Button></template>Full Width
<script setup lang="ts">import { Button } from '@hareui/vue'import { Icon } from '@iconify/vue'</script><template> <div class="w-[400px] space-y-3"> <Button full-width>Primary Button</Button> <Button full-width> <Icon icon="gravity-ui:plus" /> With Icon </Button> </div></template>Disabled State
<script setup lang="ts">import { Button } from '@hareui/vue'</script><template> <div class="flex flex-wrap gap-3"> <Button disabled>Primary</Button> <Button disabled variant="secondary">Secondary</Button> <Button disabled variant="tertiary">Tertiary</Button> <Button disabled variant="outline">Outline</Button> <Button disabled variant="ghost">Ghost</Button> <Button disabled variant="danger">Danger</Button> </div></template>Social Buttons
<script setup lang="ts">import { Button } from '@hareui/vue'</script><template> <div class="flex w-full max-w-xs flex-col gap-3"> <Button class="w-full" variant="tertiary"> <svg viewBox="0 0 18 18" width="1em" height="1em"> <path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" /> <path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.184l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" /> <path fill="#FBBC05" d="M3.964 10.706A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.706V4.962H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.038l3.007-2.332z" /> <path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.962L3.964 7.294C4.672 5.167 6.656 3.58 9 3.58z" /> </svg> Sign in with Google </Button> <Button class="w-full" variant="tertiary"> <svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor"> <path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.09.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.004.07 1.532 1.03 1.532 1.03.892 1.529 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.221-.253-4.555-1.111-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.268 2.75 1.025A9.564 9.564 0 0 1 12 6.844c.85.004 1.705.115 2.504.337 1.909-1.293 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.841-2.337 4.687-4.565 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.418 22 12c0-5.523-4.477-10-10-10z" /> </svg> Sign in with GitHub </Button> <Button class="w-full" variant="tertiary"> <svg viewBox="0 0 384 512" width="1em" height="1em" fill="currentColor"> <path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" /> </svg> Sign in with Apple </Button> </div></template>Render Function
<script setup lang="ts">import { Button } from '@hareui/vue'import { defineComponent, h, ref } from 'vue'const CustomButton = defineComponent((_, { attrs, slots }) => { const pressed = ref(false) const release = () => (pressed.value = false) return () => h( 'button', { ...attrs, 'type': 'button', 'data-custom': pressed.value ? 'pressed' : 'bar', 'onPointerdown': () => (pressed.value = true), 'onPointerup': release, 'onPointerleave': release, }, slots.default?.(), )})</script><template> <Button :as="CustomButton"> Press me </Button></template>Adding custom variants
You can extend HareUI's button theme by wrapping it with the tv extension mechanism, the same pattern used internally by every component theme.
<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>Adding Ripple Effect
The Button component supports ripple effects through composition, allowing you to nest ripple components as children. This example uses a Vue port of m3-ripple.
<script setup lang="ts">import { Button } from '@hareui/vue'import Ripple from '../_shared/Ripple.vue'</script><template> <Button variant="secondary"> <Ripple /> Click me </Button></template>Customization
Tailwind CSS
Use the class prop to merge classes onto the root element (tailwind-merge resolves conflicts):
<script setup lang="ts">import { Button } from '@hareui/vue'/** * The `gradient-border` class below depends on a global utility. * Add this to your global CSS before using it: * * ```css * @utility gradient-border { * &::before { * content: ""; * position: absolute; * inset: 0; * z-index: 0; * border-radius: inherit; * padding: var(--gradient-border-width, 1px); * background: var(--gradient-border); * pointer-events: none; * -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); * -webkit-mask-composite: xor; * mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); * mask-composite: exclude; * } * } * ``` * * On the component, set `--gradient-border` (gradient) and optionally * `--gradient-border-width` (default `1px`) via Tailwind arbitrary properties. */</script><template> <Button class="gradient-border relative z-0 rounded-full bg-linear-to-t from-neutral-100 to-white px-10 py-3 font-[450] text-neutral-800 shadow-none transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] [--gradient-border-width:1.5px] [--gradient-border:linear-gradient(315deg,#e5e5e5_0%,#fafafa_50%,#c4c4c4_100%)] hover:from-white hover:to-neutral-50 hover:brightness-105 active:scale-95 dark:from-neutral-900 dark:via-neutral-800 dark:to-neutral-800/80 dark:text-neutral-100 dark:[--gradient-border:linear-gradient(315deg,#404040_0%,#262626_50%,#525252_100%)] dark:hover:from-neutral-800 dark:hover:via-neutral-800 dark:hover:to-neutral-900/90" variant="ghost" > Upgrade </Button></template>Global theme override
Override the button theme app-wide with createHareUI:
app.use(createHareUI({
ui: {
button: {
slots: { base: 'rounded-md' },
defaultVariants: { variant: 'secondary' },
},
},
}))Styling Reference
Each rendered element carries the same data-slot string as HeroUI, so selectors translate 1:1.
ui key | data-slot | Description |
|---|---|---|
base | button | The root <button> / as element |
spinner | spinner | The auto-rendered pending spinner |
Interactive states
- Hover:
:hover/[data-hovered=true] - Active/Pressed:
:active - Focus:
:focus-visible - Disabled:
:disabled/[aria-disabled=true] - Pending:
[data-pending=true]— renders aSpinnerin the leading slot, blocks@click, stays focusable
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
as | AsTag | Component | "button" | The element or component to render as. |
type | "button" | "submit" | "reset" | "button" | The native button type. Only applied when rendered as a button. |
variant | "danger" | "primary" | "secondary" | "tertiary" | "ghost" | "outline" | "danger-soft" | 'primary' | Visual style of the button. |
size | "md" | "sm" | "lg" | 'md' | Size of the button. |
fullWidth | boolean | undefined | Whether the button takes the full width of its container. |
iconOnly | boolean | false | Whether the button only contains an icon (square shape). |
disabled | boolean | undefined | Whether the button is disabled. |
pending | boolean | false | Whether the button is in a pending state: shows a spinner, blocks clicks, stays focusable. |
ui | ComponentSlots<{ slots: { base: string[]; spinner: string; }; variants: { variant: { primary: { base: string; }; secondary: { base: string; }; tertiary: { base: string; }; ghost: { base: string; }; outline: { base: string; }; danger: { base: string; }; 'danger-soft': { base: string; }; }; size: { sm: { base: string; }; md: { base: string; }; lg: { base: string; }; }; iconOnly: { true: { base: string; }; }; fullWidth: { true: { base: string; }; false: { base: string; }; }; }; defaultVariants: { readonly variant: "primary"; readonly size: "md"; readonly fullWidth: false; readonly iconOnly: false; }; }> | - | Per-slot class overrides. |
Slots
| Slot | Props | Description |
|---|---|---|
leading | { pending: boolean; } | Content rendered before the label. Replaces the pending spinner. |
default | { pending: boolean; } | The button label. |
trailing | { pending: boolean; } | Content rendered after the label. |



