---
title: EmButton - Emerald Button for Vue
meta:
- name: description
  content: Emerald's button — four variants, three sizes, and a loading state that keeps the button's width while it spins. Composed on Vuetify0's headless Button.
- name: keywords
  content: emerald button, vue button, design system button, loading button, vuetify0 button, paper emerald
features:
  category: Component
  label: 'C: EmButton'
  level: 2
  renderless: false
  order: 1
related:
  - /systems/emerald
  - /systems/emerald/icon
  - /components/actions/button
---

# EmButton

<DocsPageFeatures :frontmatter />

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.

::: ds-example
/systems/emerald/button/basic
:::

## Anatomy

```vue Anatomy no-filename
<script setup lang="ts">
  import { EmButton } from '@paper/emerald'
</script>

<template>
  <EmButton />
</template>
```

## Composed on v0

`EmButton` renders v0's [Button](/components/actions/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

::: ds-example
/systems/emerald/button/sizes

### 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.
:::

::: ds-example
/systems/emerald/button/loading

### 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.
:::

::: ds-example
/systems/emerald/button/icons

### 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

<!-- Hand-authored, temporarily. These pages predate DocsApi extraction for
     @paper/* packages; once the generator covers the design systems this table
     is replaced by <DocsApi />. Keep it in sync with EmButton.vue until then. -->

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'primary' \| 'secondary' \| 'tertiary' \| 'destructive'` | `'primary'` | Visual role in the action hierarchy |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Type and padding step |
| `disabled` | `boolean` | `false` | Action unavailable. Sets the native `disabled` attribute — blocks activation and removes the button from the tab order |
| `loading` | `boolean` | `false` | Action in flight. Sets `aria-busy` and `data-loading` and covers the label with a spinner. Does **not** block activation |
| `ariaLabel` | `string` | — | Accessible name. Required for icon-only buttons |
| `name` | `string` | — | Form field name; renders a hidden input when set |
| `namespace` | `string` | — | Which 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:

| State | Attributes set | Focusable | Activation | Announced as |
|-------|----------------|-----------|------------|--------------|
| `disabled` | `disabled`, `aria-disabled`, `data-disabled` | No | Blocked | Disabled — present but unavailable |
| `loading` | `aria-busy`, `data-loading` | Yes | **Still fires** | Busy, 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.
