Skip to main content
Vuetify0 v1.0 is here
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

EmButton

Edit this page
Report a Bug
Open issues
Copy Markdown

Renders elementIntermediateAug 14, 2026

The primary action control — four variants, three sizes, and a loading state that spins in place without the button changing size.

Usage

EmButton is a shell component: fixed anatomy, so everything is a prop and the default slot is the label. variant picks the role the button plays in a layout, and size picks the type scale it sits on.

The four variants are a hierarchy, not a palette. Use exactly one primary per view — it is the thing you want the reader to do. secondary is the outlined alternative for a second, equally valid action; tertiary is text-only, for actions that should stay out of the way; destructive is for the ones that delete something.

<script setup lang="ts">
  import { EmButton } from '@paper/emerald'
</script>

<template>
  <div class="emerald-docs-row">
    <EmButton variant="primary">Save changes</EmButton>

    <EmButton variant="secondary">Preview</EmButton>

    <EmButton variant="tertiary">Cancel</EmButton>

    <EmButton variant="destructive">Delete</EmButton>
  </div>
</template>

<style>
  .emerald-docs-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--emerald-spacing-s, 12px);
  }
</style>

Anatomy

vue
<script setup lang="ts">
  import { EmButton } from '@paper/emerald'
</script>

<template>
  <EmButton />
</template>

Composed on v0

EmButton renders v0’s Button compound — Button.Root, Button.Content and Button.Loading — and adds nothing to its behavior.

The division is worth knowing because it explains the loading state. Button.Loading is a slot-only shell that renders no element of its own, so Emerald owns the absolutely-positioned wrapper inside it and the spinner within that. Button.Content stays in the flow the whole time. The result is that a loading button keeps the exact width its label gave it — the label is still laid out, just covered — so a toolbar does not reflow the moment someone clicks Save.

Button.Root is also what emits the data-disabled and data-loading attributes that every rule in Emerald’s stylesheet hangs off. Emerald never writes a state class; it styles the attributes v0 already publishes.

Examples

Sizes

size moves the button across Emerald’s body-text scale: sm sits on b3, md on b2, and lg on b1, each with the padding and weight that belong to that step. It is a token lookup, not a multiplier, so the three sizes stay aligned with the text they sit beside instead of drifting at large values.

Pick by density rather than by importance. md is the default and the right answer for most forms and page-level actions. Reach for sm inside a dense surface — a table row, a toolbar, a card footer — where a full-size control would dominate. lg is for a single, deliberate call to action, typically the one button on an empty state or a marketing panel. Importance is what variant expresses; size only says how much room the control gets.

Loading and disabled

The two props look similar and behave nothing alike. Only one of them actually stops a click.

loading says the button’s action is already running. Button.Root sets aria-busy and data-loading, Emerald swaps the cursor to progress, and the label stays laid out underneath the spinner so the control holds its width. This is the state for the gap between a click and its response — a form submitting, a record saving.

loading does not block activation. It sets no disabled attribute and removes no pointer events, so the button stays focusable, stays in the tab order, and your @click handler fires again on every further click. That is deliberate — the button is busy, not unavailable — but it means a double-click submits twice unless you do something about it. Either guard the handler (return early while the request is in flight) or pass disabled alongside loading when a second activation would be harmful.

disabled is the one that stops the click. It sets the native disabled attribute, drops the button to Emerald’s neutral tokens, removes it from pointer interaction and from the tab order. Use it when a precondition has not been met, and prefer explaining the precondition nearby: a disabled button with no visible reason is one of the most common accessibility complaints about design systems, and no amount of styling fixes it.

Setting both is not redundant — it is the combination you want whenever re-submitting would do damage.

Icons in buttons

The default slot takes any content, so an icon is just an EmIcon beside the label. The button’s own gap is a token, so the spacing is consistent without a wrapper or a margin.

Match the icon to the label’s step — size="s" next to sm and md buttons, m next to lg — and leave the icon decorative. It has no label prop here on purpose: the text beside it already names the action, and labelling the icon too would make a screen reader announce the button twice.

The icon-only button is the case that needs care. There is no text to read, so the accessible name has to come from somewhere, and that is what ariaLabel is for. An icon-only button without it is an unlabelled control — the single most common way a button fails an audit.

Props

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'tertiary' | 'destructive''primary'Visual role in the action hierarchy
size'sm' | 'md' | 'lg''md'Type and padding step
disabledbooleanfalseAction unavailable. Sets the native disabled attribute — blocks activation and removes the button from the tab order
loadingbooleanfalseAction in flight. Sets aria-busy and data-loading and covers the label with a spinner. Does not block activation
ariaLabelstringAccessible name. Required for icon-only buttons
namestringForm field name; renders a hidden input when set
namespacestringWhich v0 Button instance to bind to. Only needed when nesting

The default slot is the label. There are no named slots.

Accessibility

Button.Root renders a native <button>, so activation by Enter and Space, focus order, and the implicit button role all come from the platform rather than from JavaScript.

Naming

The accessible name is the default slot’s text. When there is no text — an icon-only button — you must pass ariaLabel, because the glyph inside is aria-hidden and contributes nothing. Never solve it by labelling the icon instead: that names the image, not the control, and leaves the button itself anonymous.

Disabled and loading

The two states are announced differently, and only one of them takes the control out of play:

StateAttributes setFocusableActivationAnnounced as
disableddisabled, aria-disabled, data-disabledNoBlockedDisabled — present but unavailable
loadingaria-busy, data-loadingYesStill firesBusy, with the name and role unchanged

aria-busy is what tells assistive technology the control is working, and Button.Root sets it as soon as loading goes true. What it does not do is announce the outcome. If the wait is more than momentary, say what happened when it ends — put the result in an aria-live region, or move focus to it. A spinner and a busy flag both describe the wait, not the answer.

Because loading leaves the button operable, a screen-reader or keyboard user can activate it again mid-request exactly as a mouse user can. Guard the handler, or add disabled.

A disabled button is unfocusable, which means a keyboard user tabbing through the form never encounters it and gets no explanation for why the action is missing. When the reason matters, prefer leaving the button enabled and reporting the problem on activation.

Focus

Every variant defines its own :focus-visible treatment against its own background — an outline for primary, secondary and destructive, and an inset ring for tertiary, which has no fill to outline against. The indicator only appears for keyboard focus, so a mouse click does not leave a ring behind.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/