Skip to content

Button

Start an action and see when it is running, with further presses blocked while the button is loading.

INV-0042

Acme Corp · due Sep 30

Design review and a prototype, €1 240.00 in all.

<script setup lang="ts">import { Download, Plus } from '@lucide/vue'import { UiStack, UiButton, UiCard } from 'grani'</script> <template>  <UiStack class="w-full max-w-md">    <div class="flex flex-wrap items-center justify-end gap-2">      <UiButton variant="outline"        ><Download aria-hidden="true" />Export the quarter as a spreadsheet</UiButton      >      <UiButton variant="secondary"><Plus aria-hidden="true" />New invoice</UiButton>    </div>    <UiCard title="INV-0042" description="Acme Corp · due Sep 30">      <p class="text-sm text-fg">Design review and a prototype, €1 240.00 in all.</p>      <template #footer>        <UiButton variant="secondary">Cancel</UiButton>        <UiButton>Save</UiButton>      </template>    </UiCard>  </UiStack></template>

Usage

<UiButton>…</UiButton>

With GraniResolver in the Vite config, a template needs no import: set it up once.

API

Name · typeDefaultDescription
State
loadingboolean= falsefalseA request is running: spinner, aria-busy, no clicks; it wins over disabled.
Behavior
typebutton | submit | reset= "button""button"Native type; submit sends the form the button sits in. A link has none.
tostringA route or a URL: the button becomes a link. An absolute address opens in a new tab; javascript: and data: are refused, and it stays a button.
target_blank | _selfWith to: _blank or _self decides the tab; unset, a new tab for an absolute address only.
relstringWith to: replaces the default noreferrer (nofollow, sponsored); noopener stays on a new tab.
disabledReasonstringWhy it cannot be pressed now, while disabled: it stays in the Tab order, refuses the press, and says the reason in a tooltip on hover, focus, a press and a tap, and as its description to a screen reader. A blank one is none.
loadingTextstringLabel while loading; the default is a translated "In progress…". Empty, the spinner stands alone (a glyph button).
pressedbooleanA toggle: aria-pressed, and on, a check and the brand frame. Unset, it is no toggle. A primary button is never one.
Look
variantoutline | primary | secondary | ghost | dashed= "primary""primary"Weight of the presentation: brand fill, gray plate, white with a border, or quiet.
tonebrand | success | warning | danger= "brand""brand"Meaning of the action; brand means none in particular. Every variant supports every tone.
sizexs | sm | mdStep of the control ladder: xs 28px for a micro action, sm 36px, md 44px for forms. Unset, the app's size (createGrani({ size })), sm by default.
blockboolean= falsefalseFull width, as in a drawer footer.
Form and access
disabledboolean= falsefalseNatively disabled: a flat plate, out of the Tab order; with disabledReason, focusable and saying why. A link turns back into a button.

Attributes are placed by the component itself: its description says where.

When to use another component

When you needTake
Text inside proseLink
A lone glyphIcon button
A choice among optionsRadio group segmented
Buttons joined into one capsuleButton group around them