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 keydata-slotDescription
basebuttonThe root <button> / as element
spinnerspinnerThe 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 a Spinner in the leading slot, blocks @click, stays focusable

API Reference

Props

PropTypeDefaultDescription
asAsTag | 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.
fullWidthbooleanundefinedWhether the button takes the full width of its container.
iconOnlybooleanfalseWhether the button only contains an icon (square shape).
disabledbooleanundefinedWhether the button is disabled.
pendingbooleanfalseWhether the button is in a pending state: shows a spinner, blocks clicks, stays focusable.
uiComponentSlots<{ 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

SlotPropsDescription
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.

On this page

No Headings