EmButton
The main action control. Four variants, three sizes, and a loading state that spins in place without changing the button’s width.
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.
Use one primary per view. secondary is the outlined second action. tertiary is text only. destructive is for deleting something.
Anatomy
<script setup lang="ts">
import { EmButton } from '@paper/emerald'
</script>
<template>
<EmButton />
</template>Composed on Vuetify0
EmButton renders v0’s Button compound — Button.Root, Button.Content and Button.Loading — and adds nothing to its behavior.
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. A loading button keeps the width its label gave it, because the label stays laid out and the spinner covers it. A toolbar does not reflow when 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 Vuetify0 already publishes.
Examples
Props
| 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.