Skip to content

Button

A button that can render as something else, with disabled semantics and a loading state.

View source View as Markdown

Button renders a <button>, or whatever element you point it at, and keeps the disabled and loading semantics correct either way. Use it wherever you would write <button>; if the button should stay pressed once activated, use Toggle.

App.vue
<template>
  <div class="flex flex-wrap items-center gap-3">
    <Button :class="style">Save</Button>
    <Button :class="style" loading>Saving…</Button>
    <Button :class="style" disabled>Disabled</Button>
  </div>
</template>

<script setup lang="ts">
import { Button } from '@maas/mirror/vue'

const style = [
  'inline-flex h-12 items-center justify-center gap-1.5 px-[1.125rem] whitespace-nowrap',
  'rounded-component-lg border-2 border-[transparent] type-component-lg',
  'bg-primary-solid text-primary-on-solid',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-solid-hover active:bg-primary-solid-active',
  'data-[loading=true]:bg-disabled-solid data-[loading=true]:text-disabled-on-solid',
  'data-[disabled=true]:bg-disabled-solid data-[disabled=true]:text-disabled-on-solid',
].join(' ')
</script>

Usage guidelines

  • A button whose content is a glyph has no accessible name, so give it an aria-label.
  • The button works out whether it renders a real <button>, so an <a> or a <div> gets the role, the tab stop and the keyboard activation without being told. Give it a real element through as, or put one in the slot under asChild.

Anatomy

Render it where you would write <button>.

NameRequiredDescription
Button true Renders a <button> unless told otherwise.
<script setup lang="ts">
import { Button } from '@maas/mirror/vue'
</script>

<template>
  <Button @click="save">Save</Button>
</template>

Examples

Rendering another element

as swaps the rendered element, while asChild merges everything onto the single child you provide. The button works out on its own whether it is still a real <button>: a string as names the element, and under asChild it inspects the child. It cannot inspect a component child, so slot the <button> element itself where the semantics matter. Getting that wrong logs unexpected_native_button in development, and the semantics stay as they were decided, because changing them after mount would break hydration.

<template>
  <Button as="a" href="/docs">Read the docs</Button>

  <Button as-child>
    <NuxtLink to="/docs">Read the docs</NuxtLink>
  </Button>
</template>

Swapping content while loading

loading arrives as a slot prop as well as an attribute, so the markup can branch on it without a second ref.

<template>
  <Button :loading="saving">
    <template #default="{ loading }">
      <app-spinner v-if="loading" />
      <span v-else>Save</span>
    </template>
  </Button>
</template>

Keeping a disabled button focusable

A disabled button leaves the tab order, which is right for a control nobody should reach. Where a tooltip has to explain why the button is off, focusableWhenDisabled keeps it reachable and reports the state through aria-disabled instead, so a tooltip or a screen reader still gets to it while activation stays blocked.

<template>
  <Button disabled focusable-when-disabled>Publish</Button>
</template>

loading already does this on its own, since a button that is busy for a moment should not move focus away mid-action.

Choosing the activation keys

A non-native button answers Enter and Space. keys narrows that, and on a native button an omitted Enter is blocked rather than ignored, since the browser would otherwise synthesise a click of its own.

<template>
  <Button :keys="['Space']" as="div">Save</Button>
</template>

API reference

Standalone. Ordinary props, no store. as and asChild come from every Mirror part and are documented in Composition.

Button

Renders a <button>.

Props

PropTypeDefault
type
'button' | 'submit' | 'reset''button'
disabled
booleanfalse
loading
booleanfalse
focusableWhenDisabled
booleanfalse
keys
Array<'Enter' | 'Space'>['Enter', 'Space']

Emits

None. Listeners pass through to the rendered element, so @click and @focus behave as they would on a plain <button>.

Slot props

PropType
disabled
boolean
loading
boolean
pressed
boolean

Data attributes

AttributeValue
data-disabled
true
data-loading
true
data-pressed
true

CSS variables

VariableDefault
--mirror-button-cursor
pointer
--mirror-button-disabled-cursor
not-allowed
--mirror-button-user-select
none
--mirror-button-tap-highlight-color
transparent

Errors

Code
unexpected_native_button

Accessibility

A native <button> needs no help. A non-native one, which is anything as or asChild renders that is not a <button>, gets role="button", tabindex="0" and both activation keys, and reports aria-disabled when it is disabled. Disabled, it leaves the tab order like a real button would, unless focusableWhenDisabled says otherwise.

KeyBehaviour
EnterActivates the button, on keydown.
SpaceActivates the button, on keyup. The keydown default is prevented.