GrButton

  • core
  • actions
  • API · 15 props

Triggers the primary action in a form, dialog or panel.

import { GrButton } from '@feugene/granularity/components/GrButton'

Overview

When to take it

  • the action changes state — save, delete, submit: the address does not change;
  • the action goes to a server — loading blocks a repeat and announces the wait;
  • the weight of the action is visible — variant and tone separate the primary, the secondary and the dangerous;
  • the button is an icon only — square together with ariaLabel: without a name such a button is nameless;
  • a link that looks like a button is needed — href/as swap the tag while keeping the semantics of a link.

When to take something else

NeedTake
The step changes the addressGrLink
There are several actions in a rowGrButtonGroup
There are many actions and they are hiddenGrDropdownMenu
An on/off state is toggledGrSwitch
The view of one piece of content is switchedGrSegmented

Variants every tone and view, rendered by the real component

Variants · GrButton
tone \ variantprimarysecondaryoutlineghostghost-border
primary
neutral
success
warning
danger
info
slate
azure

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled
  • loading

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 4 each with a live demo and its code

  • A toolbar with one loud action

    Secondary tools are variant="ghost-border": quiet in a row, outlined under the pointer. The prefix and suffix slots hold the icon, the filter count and the shortcut, so the label never collides with them. A square button with an icon alone gets its name from aria-label; the one action that creates something is filled.

    Invoices
    Northwind TradersINV-2026-0418 · Overdue€5,166.50
    Contoso LtdINV-2026-0417 · Paid€820.00
    Tailspin ToysINV-2026-0416 · Sent€310.00
  • Save and discard at the foot of a form

    type="submit" and type="reset" let the form do the work: Enter saves, and the reset is taken over to restore what is saved. While the request runs, loading keeps the button focusable and blocks a second click, and loading-text tells the screen reader what is being saved. Both buttons wait for a change — there is nothing to save or discard yet.

    Billing details
    Unsaved changes
  • A destructive action one deliberate step away

    tone="danger" marks what cannot be undone, and the outlined trigger only opens the step. The final button stays disabled until the name is typed, takes the full width with block, and shows loading while the workspace goes. Cancel returns the keyboard to the trigger through the exposed focus().

    Danger zone
    Transfer ownershipHand the workspace to another admin.
    Delete workspaceInvoices, customers and 12 members lose access.

API generated from the package · cannot drift

Props

PropTypedefaultDescription
disabledboolean | undefinedfalseThe action is unavailable: native `disabled` on a `<button>`; on a link button — `aria-disabled` and the address removed. The colours come from tokens, without opacity.
type"button" | "submit" | "reset" | undefined"button"The native `type` of the `<button>`. `button` by default: a button inside a form does not submit it on its own.
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe meaning of the action: the colour of the fill, the border and the text. `danger` is for the irreversible, `neutral` for the secondary. `primary` by default; inside `GrButtonGroup` — the tone of the group.
variantGrButtonVariant | undefinedundefinedThe weight of the button: `primary` — a fill in the tone, `secondary` — a soft background with a border, `outline` — a border only, `ghost` — text only, `ghost-border` — text whose border appears under the pointer. `primary` by default; globally — `componentDefaults`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe height and type size on the control scale. `md` by default; taken from the group and `GrConfigProvider`.
ariaLabelstring | undefinedundefinedThe accessible name of the button when there is no text: a `square` with a single icon is nameless without it.
loadingboolean | undefinedfalseThe action is in progress: a spinner in place of the prefix, `aria-busy` and the click intercepted. The button keeps the focus meanwhile — native `disabled` is not set.
loadingTextstring | undefinedundefinedi18n: what exactly is loading. `aria-busy` on its own is not announced by some AT.
squareboolean | undefinedundefinedEqual sides — a button made of a single icon. Without text it needs `ariaLabel`.
asstring | Component | undefinedundefinedPolymorphism: a root tag or component of your own (a RouterLink, for instance).
blockboolean | undefinedfalseA button across the full width of the container.
hrefstring | undefinedundefinedRenders the button as an `<a href>` (unless `as` is set).
targetstring | undefinedundefinedThe `target` of a link button. With `_blank`, `rel="noopener noreferrer"` is added automatically.
relstring | undefinedundefinedThe `rel` of a link button. Without it, `noopener noreferrer` is set when `target="_blank"`.

Slots

SlotTypeDescription
defaultanyThe content of the button.
prefixanyAn addon on the left: an icon, a counter. In the loading state it gives its place to the spinner.
suffixanyAn addon on the right: a chevron, a counter.

Methods

MethodsSignatureDescription
focus() => voidPut focus on the button — both when the root is a tag and when it is a component from `as`.
blur() => voidRemove focus from the button.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrButtonConfigurableProps, GrButtonVariant, GrButtonTone } from '@feugene/granularity/components/GrButton'

Filedist/types/components/GrButton/config.d.ts

grButtonConfigconst
const grButtonConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrButton">;

Filedist/types/components/GrButton/defaults.d.ts

GrButtonConfigurablePropsinterface
interface GrButtonConfigurableProps {
    variant: GrButtonVariant;
    tone: GrButtonTone;
    size: GrButtonSize;
    square: boolean;
}

Filedist/types/components/GrButton/grButtonStyles.d.ts

GrButtonVarianttype
type GrButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'ghost-border';
GrButtonTonetype
type GrButtonTone = GrTone;
GrButtonSizetype
type GrButtonSize = GrComponentSize;

Filedist/types/components/GrButton/safelist.d.ts

grButtonSafelistconst
const grButtonSafelist: string[];

Filedist/types/components/GrButton/GrButton.vue.d.ts

GrButtonPropsinterface
interface GrButtonProps {
    variant?: GrButtonVariant;
    tone?: GrButtonTone;
    size?: GrButtonSize;
    loading?: boolean;
    loadingText?: string;
    disabled?: boolean;
    square?: boolean;
    block?: boolean;
    type?: 'button' | 'submit' | 'reset';
    ariaLabel?: string;
    as?: string | Component;
    href?: string;
    target?: string;
    rel?: string;
    external?: boolean;
}

Filedist/types/components/GrButton/index.d.ts

GrButtonInstancetype
type GrButtonInstance = ComponentExposed<typeof GrButtonComponent>;

Usage from the library prose

disabled and loading are different states

disabled sets the native attribute (on a <button>) and dims the button with the --gr-button-disabled-bg / -fg / -brd tokens. There is no transparency here on purpose: opacity dilutes colours tuned to AA, and a button stands on a coloured background more often than other components, where diluted text is the first to fail.

Those tokens are themselves references to the shared roles --gr-disabled-bg/-fg/-brd, the same ones a tab, a segment and a slider use: the disabled state looks the same across the whole package and is recoloured from one place. The per-component trio remains a point of customisation for the case where a button has to differ.

The dimming works for a button-link too (href/as): the native disabled does not reach it, so the colours are set with classes rather than with the disabled: variant.

A button that has to stay in the tab order but is declared unavailable is marked with the aria-disabled="true" attribute. It gets the same dimmed look, the native disabled is not set, and the component does not intercept the click — the consumer suppresses it. This is how GrTransfer keeps a move that has nowhere to go: dropping focus from the pressed button into the document body would be worse.

loading does not set the native disabled — the element would be left without focus, and the screen reader would lose the context. Instead there are aria-busy, aria-disabled and click interception in the capture phase. The button looks ordinary while that happens: the state is told by the spinner.

vue
<GrButton :loading="saving" loading-text="Saving the report">
Save
</GrButton>

aria-busy on its own is not announced by every AT, so during loading a hidden suffix is added to the name of the button — loadingText or the gr.button.loading key.

Slots
vue
<GrButton>
  <template #prefix><GrIcon><IconPlus /></GrIcon></template>
  Add
  <template #suffix><GrKbd keys="mod+N" /></template>
</GrButton>

The icon and the text used to fall into one slot, and the order was held by the discipline of the consumer. During loading the spinner takes the place of the prefix and #prefix is not rendered: two icons side by side read as a layout defect.

A square button and block

square gives a button with equal sides — the size comes from --gr-button-square-size with a default per size (1.75rem / 2rem / 2.5rem / 2.75rem). The variable can be set in your own CSS to fit the button to your grid:

css
.toolbar { --gr-button-square-size: 2.25rem; }

The variable is one for all four steps. Set higher up the tree — on :root, in a theme — it collapses xs, sm, md and lg into a single number, and the step stops meaning anything. Its place is therefore a narrow scope like the example above, not the root of the document. If you need a size that depends on the input device, set it through GrConfigProvider rather than through the variable: the application knows about its pointer, the package does not.

block stretches the button to the width of the container.

The polymorphic root

as → <a href> → <button>. A link automatically gets rel="noopener noreferrer" if it opens in a new tab (external or an explicit target="_blank"), and disabled removes it from the tab order.