Button
A button that can render as something else, with disabled semantics and a loading state.
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.
<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 throughas, or put one in the slot underasChild.
Anatomy
Render it where you would write <button>.
| Name | Required | Description |
|---|---|---|
| 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
Emits
None. Listeners pass through to the rendered element, so @click and @focus
behave as they would on a plain <button>.
Slot props
Data attributes
CSS variables
Errors
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.