Skip to main content
Vuetify0 is now in alpha!
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Button

Headless button with loading state, toggle group support, and icon accessibility.


Renders elementIntermediateApr 7, 2026

Usage

The Button component renders as a native <button> by default (or an anchor, router-link, etc. via the as prop). It provides four distinct interaction states for controlling click behavior and visual feedback.

Basic Button

A basic button with a click handler and an <a>-tag variant with hover effects.

Link button

Anatomy

vue
<script setup lang="ts">
  import { Button } from '@vuetify/v0'
  import { mdiSend } from '@mdi/js'
</script>

<template>
  <Button.Root>
    <Button.Icon>
      <svg viewBox="0 0 24 24"><path :d="mdiSend" /></svg>
    </Button.Icon>

    <Button.Content>Submit</Button.Content>

    <Button.Loading>...</Button.Loading>
  </Button.Root>

  <Button.Group>
    <Button.Root>
      A
      <Button.HiddenInput name="choice" />
    </Button.Root>

    <Button.Root>
      B
      <Button.HiddenInput name="choice" />
    </Button.Root>
  </Button.Group>
</template>

Interaction States

Button supports four states that block click events. Each state has a distinct semantic meaning:

StateClick blockedFocusableHoverableTab orderUse case
disabledYesNoNoRemovedButton is not applicable
readonlyYesYesYesKeptDisplay-only, no action needed
passiveYesYesYesKeptTemporarily unavailable
loadingYesYesYesKeptWaiting for async operation

Button States

Disabled, readonly, passive, and loading states with visual styling for each.

native disabled
focusable, no click
aria-disabled
1s grace period

Data Attributes

Each state sets a corresponding data-* attribute on the element for CSS styling:

AttributeWhen set
data-disableddisabled prop is true
data-readonlyreadonly prop is true
data-passivepassive prop is true
data-loadingLoading grace period has elapsed
data-selectedButton is selected in a group
Tip

disabled uses native disabled attribute and removes the button from tab order. passive uses aria-disabled="true" instead — the button stays focusable and screen readers announce it as disabled.

Recipes

Loading with Grace Period

The loading state has a built-in 1-second grace period before showing loading UI. This prevents flicker for fast operations — if the async work completes within 1 second, the loading indicator never appears.

Use Button.Loading and Button.Content to swap between loading and default content:

Loading State with Grace Period

Loading spinner with a grace period before it appears, swapping between spinner and label with opacity transitions.

Click the button — the loading indicator appears after a 1-second grace period.

Button.Loading and Button.Content conditionally render based on the loading state. Only one is visible at a time — Content by default, Loading after the grace period elapses.

Toggle Groups

Wrap buttons in Button.Group for toggle behavior with v-model support. Each Button.Root needs a value prop to participate in selection.

Button Group Toggle

Text alignment toggle group with v-model binding showing the active selection.

Selected: none

Button.Group supports multiple for multi-select and mandatory to prevent deselecting the last item:

vue
<template>
  <!-- Multi-select -->
  <Button.Group v-model="formatting" multiple>
    <Button.Root value="bold">B</Button.Root>
    <Button.Root value="italic">I</Button.Root>
    <Button.Root value="underline">U</Button.Root>
  </Button.Group>

  <!-- Mandatory single-select -->
  <Button.Group v-model="view" mandatory>
    <Button.Root value="grid">Grid</Button.Root>
    <Button.Root value="list">List</Button.Root>
  </Button.Group>
</template>

Icon Buttons

Use Button.Icon to wrap icon content. It sets aria-hidden="true" on itself and detects icon-only buttons — warning in dev when aria-label is missing on Root.

Icon Buttons

Button with icon and label, and an icon-only button with aria-label for accessibility.

Form Submission

Use Button.HiddenInput inside a group to submit toggle state with forms. It renders a visually hidden checkbox that reflects the button’s selected state.

vue
<script setup lang="ts">
  import { Button, Form } from '@vuetify/v0'
  import { shallowRef } from 'vue'

  const answer = shallowRef<string>()

  function onSubmit () {
    console.log('Answer:', answer.value)
  }
</script>

<template>
  <Form @submit="onSubmit">
    <Button.Group v-model="answer">
      <Button.Root value="yes">
        Yes
        <Button.HiddenInput name="answer" value="yes" />
      </Button.Root>

      <Button.Root value="no">
        No
        <Button.HiddenInput name="answer" value="no" />
      </Button.Root>
    </Button.Group>

    <button type="submit">Submit</button>
  </Form>
</template>

Accessibility

Button.Root handles ARIA attributes automatically:

  • role="button" for proper semantics

  • type="button" when rendered as a <button> (prevents implicit form submission)

  • aria-pressed reflects selection state when inside a group

  • aria-disabled="true" for passive state (not native disabled)

  • aria-label from the ariaLabel prop

  • tabindex="0" for keyboard focus (-1 when disabled)

  • Native disabled attribute when disabled (removes from tab order)

For custom implementations, use renderless mode and bind the attrs slot prop:

vue
<template>
  <Button.Root v-slot="{ attrs }" renderless>
    <div v-bind="attrs">
      <!-- Custom button visual -->
    </div>
  </Button.Root>
</template>

API Reference

The following API details are for all variations of the Button component.

Button.Root

Props

disabled

boolean | undefined

Disables the button — fully non-interactive, removed from tab order

Default: false

readonly

boolean | undefined

Non-clickable but looks normal, remains focusable/hoverable

passive

boolean | undefined

Non-clickable, looks disabled via [data-passive], remains focusable/hoverable

Default: false

loading

boolean | undefined

Triggers loading state with grace period before visual indicator

Default: false

grace

number | undefined

Duration in ms before loading UI appears (0 to show immediately)

Default: 0

value

V | undefined

Value for use inside Button.Group

namespace

string | undefined

Namespace for context provision to children

Default: "v0:button:root"

groupNamespace

string | undefined

Namespace for connecting to parent Button.Group

Default: "v0:button:group"

name

string | undefined

Form field name — auto-renders HiddenInput when set

form

string | undefined

Associate with form by ID

ariaLabel

string | undefined

Accessible label for the button

Slots

default

ButtonRootSlotProps

Button.Content

Props

id

any

Unique identifier for ticket registration

Default: useId()

namespace

string | undefined

Namespace for context injection from parent Button.Root

Default: "v0:button:root"

Slots

default

ButtonContentSlotProps

Button.Group

Props

namespace

string | undefined

Namespace for dependency injection

Default: "v0:button:group"

disabled

boolean | undefined

Disables the entire button group

Default: false

multiple

boolean | undefined

Single (default) or multi-select

Default: false

mandatory

boolean | "force" | undefined

Controls mandatory selection behavior: - false (default): No mandatory enforcement - true: Prevents deselecting the last selected item - `force`: Automatically selects the first non-disabled item on registration

Default: false

label

string | undefined

Accessible name for the group

ariaLabelledby

string | undefined

ID of element that labels this group

ariaDescribedby

string | undefined

ID of element that describes this group

modelValue

T | T[] | undefined

Events

update:model-value

[value: T | T[]]

Slots

default

ButtonGroupSlotProps

Button.HiddenInput

Props

namespace

string | undefined

Namespace for context injection from parent Button.Root

Default: "v0:button:root"

name

string | undefined

Form field name

value

string | undefined

Submitted value (defaults to 'on')

form

string | undefined

Associate with form by ID

Button.Icon

Props

namespace

string | undefined

Namespace for context injection from parent Button.Root

Default: "v0:button:root"

Slots

default

ButtonIconSlotProps

Button.Loading

Props

id

any

Unique identifier for ticket registration

Default: useId()

namespace

string | undefined

Namespace for context injection from parent Button.Root

Default: "v0:button:root"

Slots

default

ButtonLoadingSlotProps
Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/