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 · type | Default | Description |
|---|---|---|
| State | ||
| loadingboolean= false | false | A 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. |
| tostring | A 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 | _self | With to: _blank or _self decides the tab; unset, a new tab for an absolute address only. | |
| relstring | With to: replaces the default noreferrer (nofollow, sponsored); noopener stays on a new tab. | |
| disabledReasonstring | Why 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. | |
| loadingTextstring | Label while loading; the default is a translated "In progress…". Empty, the spinner stands alone (a glyph button). | |
| pressedboolean | A 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 | md | Step 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= false | false | Full width, as in a drawer footer. |
| Form and access | ||
| disabledboolean= false | false | Natively 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 need | Take |
|---|---|
| Text inside prose | Link |
| A lone glyph | Icon button |
| A choice among options | Radio group segmented |
| Buttons joined into one capsule | Button group around them |