# Button

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

```slint
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 96px;
    background: Tokens.color-background;

    HorizontalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button { text: "Save changes"; }
        Button { text: "Cancel"; variant: ButtonVariant.outline; }
        Button { text: "Delete"; variant: ButtonVariant.destructive; }
    }
}
```

## Usage

```slint
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.

```slint
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 160px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        spacing: 12px;
        padding: 16px;

        HorizontalLayout {
            alignment: center;
            spacing: 8px;

            Button { text: "Default"; }
            Button { text: "Outline"; variant: ButtonVariant.outline; }
            Button { text: "Secondary"; variant: ButtonVariant.secondary; }
            Button { text: "Ghost"; variant: ButtonVariant.ghost; }
        }

        HorizontalLayout {
            alignment: center;
            spacing: 8px;

            Button { text: "Link"; variant: ButtonVariant.link; }
            Button { text: "Destructive"; variant: ButtonVariant.destructive; }
            Button { text: "Glow"; variant: ButtonVariant.glow; }
            Button { text: "Glass"; variant: ButtonVariant.glass; }
        }
    }
}
```

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

```slint
import { Button, ButtonSize } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 96px;
    background: Tokens.color-background;

    HorizontalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button { text: "Extra small"; size: ButtonSize.xs; }
        Button { text: "Small"; size: ButtonSize.sm; }
        Button { text: "Default"; }
        Button { text: "Large"; size: ButtonSize.lg; }
    }
}
```

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

```slint
import { Button, ButtonJoin, ButtonVariant } from "@glint/components/button.slint";
import { ButtonGroupColumn } from "@glint/components/button-group.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 176px;
    background: Tokens.color-background;

    HorizontalLayout {
        alignment: center;
        padding: 16px;

        ButtonGroupColumn {
            accessible-label: "Zoom";

            Button { text: "Zoom in"; variant: ButtonVariant.outline; join: ButtonJoin.first; }
            Button { text: "Reset"; variant: ButtonVariant.outline; join: ButtonJoin.middle; }
            Button { text: "Zoom out"; variant: ButtonVariant.outline; join: ButtonJoin.last; }
        }
    }
}
```

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

```slint
import { Button, ButtonSize, ButtonVariant } from "@glint/components/button.slint";
import { Icon } from "@glint/components/icon.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 480px;
    height: 96px;
    background: Tokens.color-background;

    HorizontalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button {
            size: ButtonSize.icon;
            accessible-label: "Add item";

            Icon {
                icon: IconSet.Plus;
                tint: Tokens.color-primary-foreground;
            }
        }

        Button {
            size: ButtonSize.icon;
            variant: ButtonVariant.outline;
            accessible-label: "Copy to clipboard";

            Icon {
                icon: IconSet.Copy;
            }
        }

        Button {
            size: ButtonSize.icon-sm;
            variant: ButtonVariant.ghost;
            accessible-label: "Open settings";

            Icon {
                icon: IconSet.Settings;
                size: 14px;
            }
        }
    }
}
```

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

```slint
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 96px;
    background: Tokens.color-background;

    in-out property <bool> saving: false;

    HorizontalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button {
            text: "Save changes";
            loading: root.saving;
            clicked => { root.saving = true; }
        }

        Button {
            text: "Reset";
            variant: ButtonVariant.outline;
            clicked => { root.saving = false; }
        }
    }
}
```

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

```slint
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 96px;
    background: Tokens.color-background;

    HorizontalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button { text: "Publish"; disabled: true; }
        Button { text: "Discard"; variant: ButtonVariant.outline; disabled: true; }
    }
}
```

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

```slint
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 320px;
    height: 128px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button { text: "Continue"; }
        Button { text: "Back"; variant: ButtonVariant.ghost; }
    }
}
```

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

```slint
import { Button } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 96px;
    background: Tokens.color-background;

    in-out property <int> count: 0;

    HorizontalLayout {
        alignment: center;
        spacing: 8px;
        padding: 16px;

        Button {
            text: root.count == 0 ? "Not clicked yet" : "Clicked " + root.count + "×";
            clicked => { root.count += 1; }
        }
    }
}
```

## API Reference

### Properties

| Property        | Type               | Default                 | Description                                                                                                                                                                                                                                                                              |
| --------------- | ------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `variant`       | `in ButtonVariant` | `ButtonVariant.default` | Visual style — see `ButtonVariant`.                                                                                                                                                                                                                                                      |
| `size`          | `in ButtonSize`    | `ButtonSize.default`    | Height + horizontal padding preset — see `ButtonSize`, whose `icon-*` members are the square, label-less form of each height.                                                                                                                                                            |
| `text`          | `in string`        | no default              | Label rendered inside the button.                                                                                                                                                                                                                                                        |
| `leading-icon`  | `in LucideIcon`    | no default              | Glyph before the label, normally selected from `IconSet` in lucide-slint. A loading button shows its spinner in this place instead.                                                                                                                                                      |
| `trailing-icon` | `in LucideIcon`    | no default              | Glyph after the label, normally selected from `IconSet` in lucide-slint.                                                                                                                                                                                                                 |
| `join`          | `in ButtonJoin`    | `ButtonJoin.none`       | Where this button sits in a welded ButtonGroup — see `ButtonJoin`.                                                                                                                                                                                                                       |
| `disabled`      | `in bool`          | `false`                 | When true, the button dims and stops firing `clicked`.                                                                                                                                                                                                                                   |
| `loading`       | `in bool`          | `false`                 | When true, indicates busy/loading state and prevents interaction.                                                                                                                                                                                                                        |
| `loading-label` | `in 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`).                                                                                                          |
| `haspopup`      | `in bool`          | `false`                 | When true, this button opens a popup/menu; like vega's aria-haspopup it opts out of the 1px press dip.                                                                                                                                                                                   |
| `focus-visible` | `out bool`         | no default              | Whether 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-held`    | `out bool`         | no default              | The 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`.                                                                                 |
| `focused`       | `out bool`         | no default              | Whether 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

| Callback    | Description                           |
| ----------- | ------------------------------------- |
| `clicked()` | Fired on press release while enabled. |

### Functions

| Function                | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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

| Enum            | Values                                                                             |
| --------------- | ---------------------------------------------------------------------------------- |
| `ButtonVariant` | `default`, `outline`, `secondary`, `ghost`, `link`, `destructive`, `glow`, `glass` |
| `ButtonSize`    | `xs`, `sm`, `default`, `lg`, `icon`, `icon-xs`, `icon-sm`, `icon-lg`               |
| `ButtonJoin`    | `none`, `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.
