Button
Headless button with loading state, toggle group support, and icon accessibility.
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.
Anatomy
<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:
| State | Click blocked | Focusable | Hoverable | Tab order | Use case |
|---|---|---|---|---|---|
| disabled | Yes | No | No | Removed | Button is not applicable |
| readonly | Yes | Yes | Yes | Kept | Display-only, no action needed |
| passive | Yes | Yes | Yes | Kept | Temporarily unavailable |
| loading | Yes | Yes | Yes | Kept | Waiting for async operation |
Button States
Disabled, readonly, passive, and loading states with visual styling for each.
Data Attributes
Each state sets a corresponding data-* attribute on the element for CSS styling:
| Attribute | When set |
|---|---|
data-disabled | disabled prop is true |
data-readonly | readonly prop is true |
data-passive | passive prop is true |
data-loading | Loading grace period has elapsed |
data-selected | Button is selected in a group |
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:
<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.
<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 semanticstype="button"when rendered as a<button>(prevents implicit form submission)aria-pressedreflects selection state when inside a grouparia-disabled="true"for passive state (not native disabled)aria-labelfrom theariaLabelproptabindex="0"for keyboard focus (-1when disabled)Native
disabledattribute when disabled (removes from tab order)
For custom implementations, use renderless mode and bind the attrs slot prop:
<template>
<Button.Root v-slot="{ attrs }" renderless>
<div v-bind="attrs">
<!-- Custom button visual -->
</div>
</Button.Root>
</template>Button.Root
Props
disabled
boolean | undefinedDisables the button — fully non-interactive, removed from tab order
Default: false
passive
boolean | undefinedNon-clickable, looks disabled via [data-passive], remains focusable/hoverable
Default: false
loading
boolean | undefinedTriggers loading state with grace period before visual indicator
Default: false
groupNamespace
string | undefinedNamespace for connecting to parent Button.Group
Default: "v0:button:group"
Slots
default
ButtonRootSlotPropsButton.Content
Props
namespace
string | undefinedNamespace for context injection from parent Button.Root
Default: "v0:button:root"
Slots
default
ButtonContentSlotPropsButton.Group
Props
mandatory
boolean | "force" | undefinedControls 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
modelValue
T | T[] | undefinedEvents
update:model-value
[value: T | T[]]Slots
default
ButtonGroupSlotPropsButton.HiddenInput
Props
namespace
string | undefinedNamespace for context injection from parent Button.Root
Default: "v0:button:root"
Button.Icon
Props
namespace
string | undefinedNamespace for context injection from parent Button.Root
Default: "v0:button:root"
Slots
default
ButtonIconSlotPropsButton.Loading
Props
namespace
string | undefinedNamespace for context injection from parent Button.Root
Default: "v0:button:root"
Slots
default
ButtonLoadingSlotProps