Vize

Button

Headless Button; covers shadcn/ui Button, Reka UI Primitive, React Aria Button.

Package @vizejs/ui/button
Maturity stable
Quality gates behavior contract, mounted dom, type inference, tree shaking, bundle size, ssr, hydration, dom compile, ssr compile, vapor compile
Bundle budget 1,000 B JS gzip / 0 B CSS gzip
Own the source vize lib pull button
Requires primitive, press
Aliases action-button, button primitive
Covers shadcn/ui Button, Reka UI Primitive, React Aria Button

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/ui

Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.

import { Button } from "@vizejs/ui/button";

Or copy the source into your project with vize lib pull button (see Source Distribution).

Try it

This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.

Open the interactive example · Styling these components

Button example rendered in a real browser

Copy the example

<!-- Native buttons showing press handling, a loading state, and a disabled action. -->
<script setup lang="ts">
import { ref } from "vue";

import { Button } from "@vizejs/ui/button";

const saves = ref(0);
</script>

<template>
  <div>
    <Button type="button" @press="() => (saves += 1)">Save draft</Button>
    <Button v-slot="{ loading }" type="button" variant="outline" tone="neutral" loading>
      {{ loading ? "Uploading…" : "Upload" }}
    </Button>
    <Button type="button" variant="quiet" tone="danger" disabled>Publish</Button>
    <output>Saved {{ saves }} times</output>
  </div>
</template>

Design the action before the surface

Use a native <button> whenever the action can be expressed by one. Button keeps the platform's Enter, Space, form, focus, and disabled behavior while adding one typed press event. Use as only when composition requires another element or component, and set native to describe whether that target already implements button semantics.

<script setup lang="ts">
import { Button } from "@vizejs/ui/button";
</script>

<template>
  <Button type="submit">Save profile</Button>
  <Button type="button" variant="outline" tone="neutral">Preview</Button>
  <Button type="button" variant="quiet" tone="danger">Delete</Button>
</template>

press always carries the activation MouseEvent, whether the user activates with a pointer, Enter, or Space. That gives an application one handler without erasing native event timing.

Model unavailable states deliberately

disabled and loading are different product states:

State Focus Activation Accessible signal
available yes emits once native button semantics
loading yes suppressed aria-busy and aria-disabled
disabled no suppressed native disabled, or ARIA for non-native

Keeping a loading action focusable prevents focus from disappearing while an operation is in flight. Use the typed slot state to keep its label truthful:

<Button v-slot="{ loading }" type="button" :loading="saving" @press="save">
  {{ loading ? "Saving…" : "Save changes" }}
</Button>

Choose a visual voice, then customize it

Behavior is headless. The optional stylesheet consumes only semantic data attributes and public custom properties:

import "@vizejs/ui/base.css";
import "@vizejs/ui/theme-preset-paper.css";
import "@vizejs/ui/component-button.css";
<main data-vize-theme="paper">…</main>

variant, tone, and size are closed TypeScript unions and stable styling hooks. Presets can change geometry, typography, depth, and motion without changing the component contract. Override the component through its public properties instead of matching implementation structure:

.checkout-actions {
  --vize-ui-button-radius: 0.5rem;
  --vize-ui-button-shadow: none;
  --vize-ui-button-hover-lift: 0px;

  [data-vize-ui="button"] {
    letter-spacing: 0.01em;
  }
}

The shipped CSS is authored with native nesting and lowered with Lightning CSS. Consumers receive ordinary CSS and do not need Tailwind, a CSS-in-JS runtime, or the Vize build pipeline. Application CSS outside Vize's cascade layers wins normally. See UI styles for every preset and token boundary.

Accessibility is policy, not a preset

Every visual voice retains a visible :focus-visible ring, a 44 CSS-pixel minimum target on coarse pointers, forced-colors mappings, and a prefers-reduced-motion path. The loading indicator stops animating under reduced motion while the textual state remains available.

An icon-only button still needs an accessible name. Provide aria-label or visible text; the component does not guess a label from an icon.

Render it in any supported Vue host

The SFC has no browser-only setup path. The same public import is used for DOM, SSR, hydration, and Vapor compilation. Keep the Vue runtime, compiler, and server renderer on a compatible version when using Vapor; see the Vapor guide for the currently qualified pairing.

Host Use
Vue / Vite+ Import the component subpath in any SFC.
Nuxt Import it directly, or enable the @vizejs/nuxt UI auto-registration. See Nuxt.
Void Import it in an @void/vue page; component CSS stays in Void's route CSS graph. See Void.

For maximum ownership, vize lib pull button copies the SFC unit into the application. The public package import and the source-distributed form share the same behavior contract and tests.

Bundle only what the screen uses

Import from @vizejs/ui/button, not the package root, when the screen needs only Button. Its JavaScript budget is enforced on a production consumer, and the visual CSS is a separate opt-in asset. Importing the behavior never silently adds a theme or another component's stylesheet.

API

Button

Source: src/components/button/button.vue

Props

Prop Type Default Description
as PrimitiveAs "button" Native element, custom element, or component to render.
native boolean true when as is "button"; otherwise false Whether the rendered target already implements native button semantics.
type "button" | "reset" | "submit" "button" Native button submission behavior.
disabled boolean false Remove the control from activation and sequential keyboard focus.
loading boolean false Announce in-progress work and prevent repeated activation while preserving focus.
variant ButtonVariant "solid" Stable styling hook for filled, bordered, or low-emphasis actions.
tone ButtonTone "accent" Semantic styling hook. It never changes button behavior or accessible semantics.
size ButtonSize "md" Stable sizing hook. Coarse pointers retain the accessible minimum target size.

Events

Event Payload Description
press [event: MouseEvent] Fired when pointer or keyboard activation reaches the rendered control. Carries the resulting MouseEvent.

Slots

Slot Slot props Description
default ButtonSlotProps Renders the button contents with current availability state.

Exposed

Member Type Description
element Readonly<ShallowRef<HTMLElement | null>> Template ref to the rendered root element.
focus —

Behavior

Normative state × input → outcome table for src/components/button/button.vue (@vizejs/ui/button). Every row is proven by the named mounted-DOM test in src/components/button/tests/behavior.test.ts; a row without a passing test is a contract violation.

# State Input Outcome Proven by
B1 idle, native render role=button, accessible name from slot, type="button", data-state="idle" renders a native button with an accessible name
B2 idle Tab control receives focus joins the tab order and focuses programmatically
B3 idle exposed focus() control receives focus joins the tab order and focuses programmatically
B4 idle pointer click exactly one press carrying the MouseEvent pointer click fires exactly one press
B5 idle, native Enter exactly one press (activation on keydown) Enter and Space each fire exactly one press on a native button
B6 idle, native Space exactly one press (activation on keyup) Enter and Space each fire exactly one press on a native button
B7 idle, non-native Enter the component itself clicks the element; exactly one press Enter and Space activate a non-native button through its own handlers
B8 idle, non-native Space keydown canceled (no page scroll); one press on keyup Enter and Space activate a non-native button through its own handlers
B9 idle, non-native Escape ignored: not canceled, no press Enter and Space activate a non-native button through its own handlers
B10 idle click, Enter, Space three press emits in dispatch order, each indistinguishably a MouseEvent keyboard and pointer presses are indistinguishable MouseEvents in order
B11 disabled, native render native disabled attribute, no aria-disabled mirror, data-state="disabled" disabled native button removes activation and keeps native semantics
B12 disabled, native click / Enter / Space no press disabled native button removes activation and keeps native semantics
B13 disabled, native Tab skipped by the tab order disabled native button removes activation and keeps native semantics
B14 disabled, non-native render tabindex="-1", aria-disabled="true" disabled non-native button leaves the tab order and announces aria-disabled
B15 disabled, non-native click / Enter / Space / Tab no press; skipped by the tab order disabled non-native button leaves the tab order and announces aria-disabled
B16 loading render aria-busy="true", aria-disabled="true", data-state="loading", focusable loading button announces busy, stays focusable, and suppresses press
B17 loading focus() focus is accepted and retained loading button announces busy, stays focusable, and suppresses press
B18 loading click / Enter / Space no press loading button announces busy, stays focusable, and suppresses press
B19 any default slot render slot receives live disabled, loading, unavailable state exposes live availability to the default slot
B20 any render visual hooks literal variant, tone, and size values publish as data attributes without changing native semantics publishes typed visual hooks without changing native semantics

Keyboard activation timing (Enter on keydown, Space on keyup, Space keydown canceled) is additionally pinned as pure logic by matches native keyboard activation timing.