WlButton
Change props to see the result. The example and code use the same settings.
Settings
The “Add” label goes in the defaultslot; the icon plus goes in icon.
Example
Clicks: 0
Loading replaces the icon with a spinner and blocks repeated clicks. Disabled also blocks the action. This example has no form: submit and reset change the type attribute; check form behaviour in your application.
Application code
Vue SFC · current settings
<script setup lang="ts">
import { WlButton, WlIcon } from "gavia-ui";
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<div class="wl-stack" data-space="md">
<div>
<WlButton v-bind='{"variant":"secondary","size":"md","density":"default","loading":false,"disabled":false,"block":false,"type":"button"}' @click="count++"><template #icon><WlIcon name="plus" /></template>{{ "Add" }}</WlButton>
</div>
<p role="status" class="wl-text-small">{{ "Clicks:" }} {{ count }}</p>
</div>
</template>All variants
Compare variant values for primary, secondary and delete actions.
primarysecondaryghostsoftlinkprimary + dangerghost + dangersoft + dangerChoose any variant: all eight buttons perform a local action.
Vue SFC · application example
<script setup lang="ts">
import { WlButton, type WlButtonVariant } from "gavia-ui";
import { ref } from "vue";
// The public union makes a missing or unsupported variant a type error.
const labels = {
primary: "Create",
secondary: "Draft",
ghost: "Cancel",
soft: "Share",
link: "Learn more"
} satisfies Record<WlButtonVariant, string>;
const variants = Object.keys(labels) as WlButtonVariant[];
const lastAction = ref("");
</script>
<template>
<div class="wl-stack" data-space="lg">
<div class="button-variant-grid">
<div
v-for="variant in variants"
:key="variant"
class="wl-stack"
data-space="sm"
>
<div><WlButton :variant="variant" @click="lastAction = labels[variant]">{{ labels[variant] }}</WlButton></div>
<code class="wl-text-small">{{ variant }}</code>
</div>
<div class="wl-stack" data-space="sm">
<div>
<WlButton variant="primary" tone="danger" @click="lastAction = 'Delete'">
{{ "Delete" }}
</WlButton>
</div>
<code class="wl-text-small">primary + danger</code>
</div>
<div class="wl-stack" data-space="sm">
<div>
<WlButton variant="ghost" tone="danger" @click="lastAction = 'Delete quietly'">
{{ "Delete quietly" }}
</WlButton>
</div>
<code class="wl-text-small">ghost + danger</code>
</div>
<div class="wl-stack" data-space="sm">
<div>
<WlButton variant="soft" tone="danger" @click="lastAction = 'Archive'">
{{ "Archive" }}
</WlButton>
</div>
<code class="wl-text-small">soft + danger</code>
</div>
</div>
<p class="wl-text-small wl-text-muted" role="status">
{{ lastAction
? "Last action: " + lastAction
: "Choose any variant: all eight buttons perform a local action." }}
</p>
</div>
</template>
<style scoped>
.button-variant-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
gap: var(--wl-space-lg);
align-items: start;
}
</style>Sizes and density
Four sizes at regular and compact density. Compare both or hide the compact row.
Default density
Compact density
size selects the size; density="compact" reduces the height. Sizes come from the selected theme's tokens.
Vue SFC · application example
<script setup lang="ts">
import { WlButton, WlCheckbox, type WlDensity, type WlSize } from "gavia-ui";
import { computed, ref } from "vue";
const labels = { xs: "XS", sm: "SM", md: "MD", lg: "LG" } satisfies Record<WlSize, string>;
const sizes = Object.keys(labels) as WlSize[];
const showCompact = ref(true);
const densities = computed<readonly WlDensity[]>(() => showCompact.value ? ["default", "compact"] : ["default"]);
</script>
<template>
<div class="wl-stack" data-space="lg">
<WlCheckbox v-model="showCompact">{{ "Show compact row" }}</WlCheckbox>
<div
v-for="density in densities"
:key="density"
class="wl-stack"
data-space="sm"
:data-button-density="density"
>
<h3 class="wl-text-label">{{ density === 'compact' ? "Compact density" : "Default density" }}</h3>
<div class="wl-inline" data-space="md">
<WlButton
v-for="size in sizes"
:key="size"
:size="size"
:density="density"
variant="primary"
>{{ labels[size] }}</WlButton>
</div>
</div>
<p class="wl-text-small wl-text-muted">{{ "size selects the size; density=\"compact\" reduces the height. Sizes come from the selected theme's tokens." }}</p>
</div>
</template>Disabled and loading
Disabled and loading, plus a save example with completion, cancellation and retry.
Disabled blocks the action. Loading also blocks the button and replaces the icon slot with a spinner.
No requests are sent. Complete or cancel this example manually.
Click Save, then complete or cancel the local loading state. Saves: 0.
Vue SFC · application example
<script setup lang="ts">
import { WlButton, WlCheckbox, WlIcon } from "gavia-ui";
import { ref } from "vue";
const noAccess = ref(false);
const loading = ref(false);
const saved = ref(0);
const message = ref("Click Save, then complete or cancel the local loading state.");
function finish(): void {
loading.value = false;
saved.value++;
message.value = "Saving completed.";
}
function cancel(): void {
loading.value = false;
message.value = "Loading cancelled. You can save again.";
}
</script>
<template>
<div class="wl-stack" data-space="lg">
<div class="wl-inline" data-space="md">
<WlButton disabled aria-describedby="button-disabled-reason">{{ "Unavailable" }}</WlButton>
<WlButton variant="primary" loading>{{ "Saving…" }}</WlButton>
</div>
<p id="button-disabled-reason" class="wl-text-small wl-text-muted">
{{ "Disabled blocks the action. Loading also blocks the button and replaces the icon slot with a spinner." }}
</p>
<WlCheckbox v-model="noAccess">{{ "No permission to save" }}</WlCheckbox>
<div class="wl-inline" data-space="sm">
<WlButton
variant="primary"
:disabled="noAccess"
:loading="loading"
aria-describedby="button-saving-reason"
@click="loading = true"
>
<template #icon><WlIcon name="check" :size="16" /></template>
{{ loading ? "Saving…" : "Save" }}
</WlButton>
<WlButton v-if="loading" @click="finish">{{ "Complete saving" }}</WlButton>
<WlButton v-if="loading" variant="ghost" @click="cancel">{{ "Cancel upload" }}</WlButton>
</div>
<p id="button-saving-reason" class="wl-text-small wl-text-muted">
{{ noAccess
? "This action requires editing permission."
: "No requests are sent. Complete or cancel this example manually." }}
</p>
<p class="wl-text-small" role="status">{{ message }} {{ "Saves:" }} {{ saved }}.</p>
</div>
</template>Icons and slot content
Leading and trailing icons, an icon-only button, and a counter in the default slot.
icon places an icon on the left. The default slot can hold an icon on the right or a counter. A textless button receives an aria-label.
Click a button and check its action and accessible name.
Vue SFC · application example
<script setup lang="ts">
import { WlButton, WlIcon } from "gavia-ui";
import { ref } from "vue";
const lastAction = ref("");
const unread = ref(3);
function readMessages(): void {
unread.value = 0;
lastAction.value = "Messages marked as read";
}
</script>
<template>
<div class="wl-stack" data-space="lg">
<div class="wl-inline" data-space="md">
<WlButton variant="primary" @click="lastAction = 'New task'">
<template #icon><WlIcon name="plus" :size="16" /></template>
{{ "New task" }}
</WlButton>
<WlButton @click="lastAction = 'Find'">
<template #icon><WlIcon name="search" :size="16" /></template>
{{ "Find" }}
</WlButton>
<WlButton @click="lastAction = 'Continue'">
<span class="button-slot-inline">{{ "Continue" }}<WlIcon name="arrow-right" :size="16" /></span>
</WlButton>
<WlButton
variant="ghost"
tone="danger"
aria-label="Delete draft"
@click="lastAction = 'Draft deleted'"
>
<template #icon><WlIcon name="trash" :size="16" /></template>
</WlButton>
<WlButton variant="soft" @click="readMessages">
<span class="button-slot-inline">{{ "Messages" }}<strong>{{ unread }}</strong></span>
</WlButton>
</div>
<p class="wl-text-small wl-text-muted">{{ "icon places an icon on the left. The default slot can hold an icon on the right or a counter. A textless button receives an aria-label." }}</p>
<p class="wl-text-small" role="status">
{{ lastAction
? "Last action: " + lastAction
: "Click a button and check its action and accessible name." }}
</p>
</div>
</template>
<style scoped>
.button-slot-inline { display: inline-flex; align-items: center; gap: var(--wl-space-sm); }
</style>Button width and form submission
Toggle block, complete the required field, and compare button, submit and reset.
The form has not been submitted yet. Submissions: 0. Draft checks: 0.
A regular WlButton uses type="button" and does not submit the form. Set submit and reset explicitly. This example stores the result locally.
Vue SFC · application example
<script setup lang="ts">
import { WlButton, WlCheckbox, WlInput } from "gavia-ui";
import { ref } from "vue";
const name = ref("");
const block = ref(true);
const submitted = ref(0);
const drafts = ref(0);
const message = ref("The form has not been submitted yet.");
function submit(): void {
submitted.value++;
message.value = "Created project «" + name.value.trim() + "».";
}
function reset(): void {
name.value = "";
message.value = "Form cleared.";
}
</script>
<template>
<div class="wl-stack" data-space="lg">
<WlCheckbox v-model="block">{{ "Full-width submit button" }}</WlCheckbox>
<form
class="button-form-example wl-stack"
data-space="md"
aria-label="Create a project"
@submit.prevent="submit"
@reset.prevent="reset"
>
<div class="wl-stack" data-space="xs">
<label for="button-form-name" class="wl-text-label">{{ "Project title" }}</label>
<WlInput
id="button-form-name"
v-model="name"
name="projectName"
required
minlength="3"
pattern=".*\S.*"
aria-describedby="button-form-hint"
/>
<p id="button-form-hint" class="wl-text-small wl-text-muted">
{{ "At least three characters. Native validation runs before submit." }}
</p>
</div>
<div data-button-form-submit>
<WlButton type="submit" variant="primary" :block="block">{{ "Create project" }}</WlButton>
</div>
<div class="wl-inline" data-space="sm">
<WlButton @click="drafts++">{{ "Check draft" }}</WlButton>
<WlButton type="reset" variant="ghost">{{ "Reset form" }}</WlButton>
</div>
</form>
<p class="wl-text-small" role="status">
{{ message }} {{ "Submissions:" }} {{ submitted }}{{ ". Draft checks:" }} {{ drafts }}.
</p>
<p class="wl-text-small wl-text-muted">{{ "A regular WlButton uses type=\"button\" and does not submit the form. Set submit and reset explicitly. This example stores the result locally." }}</p>
</div>
</template>
<style scoped>
.button-form-example { width: 100%; max-width: 28rem; }
</style>When to use
- primary is the group's primary action; secondary is secondary.
- ghost and link are less prominent actions; tone="danger" marks actions that may delete data.
- block fills the available width; size and density change dimensions and spacing while preserving the DOM contract.
The full variant list comes from the manifest in settings and API. Use a link for URL navigation; WlButton renders a native button.
Props
Types, accepted values and defaults come from the public manifest. “—” means the prop is not set.
| Prop | Type / values | Default | Purpose |
|---|---|---|---|
variant | WlButtonVariant | "secondary" | Visual button variant. |
tone | WlTone | — | Semantic color, independent of the visual treatment. |
size | WlSize | "md" | Button size. |
density | WlDensity | "default" | Compact mode reduces button height. |
loading | boolean | false | Shows loading instead of the icon and blocks clicks. |
disabled | boolean | false | Disables the button. |
block | boolean | false | Fill the container width. |
type | "button" | | "button" | Native button type. |
pt | WlPt< | — | Attributes of internal Gavia UI elements. |
as | "button" | | — | Render a link explicitly; href or to also selects link rendering. |
href | string | — | Link destination, forwarded to WlLink. |
to | unknown | — | Application router destination handled by the configured link adapter. |
iconPosition | "start" | | — | Place the icon before or after the default slot. |
Events
| Event | Payload | When emitted |
|---|---|---|
click | [event: MouseEvent] | Click (not emitted when disabled/loading). |
Slots
| Slot | Type / values | Content |
|---|---|---|
default | — | Text or other button content. |
icon | — | Leading icon (hidden while loading). |
Pass-through: pt
Sections WlButton are listed below. Attributes merge in this order: createWlPt() WlConfig.pt.buttonpt instance. Class and style merge; the last supplied value takes priority for other attributes.
| Section | DOM | Purpose |
|---|---|---|
root | button | Root button attributes, additional class and style. |
label | span.wl-btn__label | Default slot wrapper. Appears when content is passed to the slot. |
loadingIcon | span.wl-btn__spinner | Loading spinner; hidden from assistive technology. |
The icon is passed through the iconslot; it has no separate pt section. Class, style, aria-* and data-* can be passed to the button itself. The root preserves data-wl="button", data-variant, data-size and data-density.
Keyboard and states
Keyboard and application responsibilities
| Key | Action |
|---|---|
| Tab / Shift+Tab | Move between enabled links, actions and form controls. |
| Enter / Space | Activate native actions; links follow with Enter. |
Provide a meaningful accessible name and connect labels, hints and errors to the focusable element. Check the behavior with your own data and a screen reader.
WAI-ARIA Authoring Practices- Tab focuses an available button; Enter or Space runs its action. Focus is visible in every theme.
- When
disabledorloadingthe nativedisabledblocks clicks and removes the button from the Tab order. loadingsetsaria-busy="true". The spinner hasaria-hidden="true"; the button text remains its accessible name.- Explain why an action is unavailable next to it. Do not convey meaning through colour or an icon alone.
- An icon-only button needs an accessible name, such as
aria-label. WlIconButton also supports this case. - Inside a form, set
type="submit"ortype="reset"deliberately; the default istype="button", which does not submit the form.
Check this page: return to the examples, select “Focus the example”, press Enter/Space and check the counter. Then enable disabled or loading: the action should stop running.