Gavia UIv1.0.0

WlButton

Action button with variants, sizes and loading state.
Components / Actions v0.1.0

Change props to see the result. The example and code use the same settings.

Settings

variantVisual button variant.
toneSemantic color, independent of the visual treatment.
sizeButton size.
densityCompact mode reduces button height.
typeNative button type.

The “Add” label goes in the defaultslot; the icon plus goes in icon.

Example

Clicks: 0

Tab, then Enter or Space

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.

primary
secondary
ghost
soft
link
primary + danger
ghost + danger
soft + danger

Choose 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.

At least three characters. Native validation runs before submit.

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.