Skip to content
Glint UI

Button

A pressable control that runs an action, in eight variants and four sizes, each with a square icon form.

Usage

import { Button, ButtonSize, ButtonVariant } from "@glint/components/button.slint";
Button {
text: "Delete project";
variant: ButtonVariant.destructive;
size: ButtonSize.sm;
disabled: root.is-saving;
clicked => { root.delete-project(); }
}

That is a fragment to drop into a component of your own: is-saving and delete-project are yours, not Glint’s.

A Button sizes itself to its label. Put it in a layout that stretches and it fills the space it is given; put it in one that does not and it stays at its content width. clicked fires on release, and only while the button is enabled and not loading — the guard is in the component, so a handler never has to repeat it.

Examples

Variants

Eight of them, chosen by meaning rather than by color: default for the one action a screen is about, outline and secondary for the ones beside it, ghost and link for actions that should not draw the eye, destructive for what cannot be undone, and glow and glass for surfaces that call for them.

Sizes

xs, sm, default and lg set the height and the horizontal padding; the label follows the reader’s font size, because Glint’s typography tokens are rem-based.

Welded into a stack

join says where a button sits in a welded column: first, middle and last square the corners that face a neighbor, and none — the default — leaves all four rounded. The seam belongs to ButtonGroupColumn, which overlaps its children by the hairline they share; the corners belong to each button, because Slint gives a parent no way to reach into its children.

Icon buttons

The icon, icon-xs, icon-sm and icon-lg sizes keep a square minimum and take the icon as a child. A button with no label has no accessible name, so name it: accessible-label is set from the call site, and the role, the enabled state and the guarded default action still come from Button itself.

Loading

A loading button keeps its label and gains a leading spinner, and every activation path goes inert until the work finishes. It gets wider by the width of the spinner: a button that must not reflow should be given a fixed width.

Disabled

A disabled button dims, stops firing clicked and leaves the tab order, while staying in the accessibility tree so it can still be found and read.

Filling the width it is given

Inside a stretching layout a Button widens to the space available — Glint sizes it through min-width and preferred-width rather than a fixed width, so no wrapper is needed for a full-width action.

Handling a click

clicked is the whole interface between the button and the application: the component decides when a press counts, and the handler decides what it does.

API Reference

Properties

PropertyTypeDefaultDescription
variantin ButtonVariantButtonVariant.defaultVisual style — see ButtonVariant.
sizein ButtonSizeButtonSize.defaultHeight + horizontal padding preset — see ButtonSize, whose icon-* members are the square, label-less form of each height.
textin stringno defaultLabel rendered inside the button.
leading-iconin LucideIconno defaultGlyph before the label, normally selected from IconSet in lucide-slint. A loading button shows its spinner in this place instead.
trailing-iconin LucideIconno defaultGlyph after the label, normally selected from IconSet in lucide-slint.
joinin ButtonJoinButtonJoin.noneWhere this button sits in a welded ButtonGroup — see ButtonJoin.
disabledin boolfalseWhen true, the button dims and stops firing clicked.
loadingin boolfalseWhen true, indicates busy/loading state and prevents interaction.
loading-labelin string@tr("Loading")Name of the spinner a loading button shows. Overridable because the button owns that element and a consumer cannot reach it (same reason AttachmentRow exposes remove-label).
haspopupin boolfalseWhen true, this button opens a popup/menu; like vega's aria-haspopup it opts out of the 1px press dip.
focus-visibleout boolno defaultWhether this control holds the keyboard *and* got it from the keyboard — the focus-visible a hover surface opens on. Published because Slint reports focus only to the element holding it, so a Tooltip wrapping this control cannot read it off the scope inside (tooltip.slint).
focus-heldout boolno defaultThe same focus, still true while a popup has borrowed the window's — what a hover surface opened by this control has to gate on, since showing itself is what takes focus-visible away. See Tooltip.
focusedout boolno defaultWhether this control holds the keyboard at all, however it arrived. DialogPanel keys its forward Tab wrap on the close control being the last stop in the cycle, and an X focused by a click is still that stop — which is the one question focus-visible cannot answer.

Callbacks

CallbackDescription
clicked()Fired on press release while enabled.

Functions

FunctionDescription
focus-from-keyboard()Hand the control the keyboard the way a key press does, ring and all. A host that moves the focus onto a control because the user pressed something — Questionnaire stepping to the next question — cannot use focus(): Slint reports that as programmatic, which is how a host parking the keyboard looks, and the scope drops the ring for it. Published by every control that publishes focus-visible, for the same reason: the scope inside cannot be reached from outside the component.

Enums

EnumValues
ButtonVariantdefault, outline, secondary, ghost, link, destructive, glow, glass
ButtonSizexs, sm, default, lg, icon, icon-xs, icon-sm, icon-lg
ButtonJoinnone, first, middle, last

Accessibility

  • Role and name. A Button exposes itself as a button whose accessible name is its text. An icon-only button has no text to take a name from, so set accessible-label at the call site.
  • Keyboard. Tab reaches the button; Enter and Space activate it. The focus ring is keyboard-only: a press with the pointer leaves no ring behind, the way :focus-visible behaves on the web.
  • Assistive technology. Screen readers activate the button through its accessible default action, which is guarded by the same rule as the pointer and keyboard paths — a disabled or loading button cannot be fired through any of the three.
  • Disabled. The button reports itself disabled and leaves the tab order, but stays in the accessibility tree: a control a user cannot find is worse than one they find and are told is unavailable.
  • Loading. Busy is not the same as unavailable, and the accessibility tree can only say the latter. A loading button therefore reports itself disabled and carries loading-label as its description, so a screen reader says the state with the button’s own name instead of leaving it to whoever explores the spinner underneath. Override loading-label to say what is happening in your own words.
  • The label is not a second element. The text inside the button is silent in the accessibility tree — the button already announces it — so the name resolves to exactly one control.
  • Popup triggers. A button that opens a menu or a popover should set haspopup, which also drops the 1px press dip that would otherwise fight the surface opening under it.