Button
Triggers an action or event. A two-axis model — 8 semantic variants × 4 appearances (fill/outline/dashed/soft) — plus a toggle, four sizes, and icon/loading affordances.
Preview
<EsaButton>Get started</EsaButton> Variants
Eight semantic variants. ai maps to the copper AI-context palette; ghost is neutral and transparent.
<EsaButton variant="primary">Primary</EsaButton>
<EsaButton variant="secondary">Secondary</EsaButton>
<EsaButton variant="danger">Danger</EsaButton>
<EsaButton variant="success">Success</EsaButton>
<EsaButton variant="warning">Warning</EsaButton>
<EsaButton variant="info">Info</EsaButton>
<EsaButton variant="ai">AI</EsaButton>
<EsaButton variant="ghost">Ghost</EsaButton> Appearances
The appearance axis is independent of variant — any variant renders as fill, outline, dashed, or soft. soft is a neutral surface with accent-colored text that fills on hover — variant="danger" appearance="soft" is the secondary-danger button.
<EsaButton variant="danger" appearance="fill">Fill</EsaButton>
<EsaButton variant="danger" appearance="outline">Outline</EsaButton>
<EsaButton variant="danger" appearance="dashed">Dashed</EsaButton>
<EsaButton variant="danger" appearance="soft">Soft</EsaButton> Active (toggle)
Set active for a pressed/selected look (and aria-pressed) — the basis for toggle buttons.
<EsaButton appearance="outline">Default</EsaButton>
<EsaButton appearance="outline" active>Active</EsaButton> Sizes
<EsaButton size="xs">Extra small</EsaButton>
<EsaButton size="sm">Small</EsaButton>
<EsaButton size="md">Medium</EsaButton>
<EsaButton size="lg">Large</EsaButton> Icons
icon sits left of the label, iconRight sits right. They are
independent props, not two settings of one, so a button can carry both. Reach for
iconRight when the glyph is an affordance — where the action goes
(arrow-right) or that it opens something (chevron-down) — and
for icon when it labels what the action is. iconOnly
drops the text and squares the box; it uses icon and ignores
iconRight.
<EsaButton icon="plus">Leading</EsaButton>
<EsaButton iconRight="arrow-right">Continue</EsaButton>
<EsaButton appearance="outline" iconRight="chevron-down">Export</EsaButton>
<EsaButton icon="download" iconRight="chevron-down">Download</EsaButton>
<EsaButton iconOnly icon="plus" label="Add" /> States
Conditions the button is in, as opposed to what it is made of. Both are
transient and both block interaction; loading also hides the label in
place and suppresses either icon, so the width never jumps.
<EsaButton loading>Loading</EsaButton>
<EsaButton disabled>Disabled</EsaButton>
<EsaButton appearance="outline" active>Active</EsaButton> Chrome — buttons that live on an app bar
Every other variant owns a color. That is exactly what makes them unusable
on chrome: ghost resolves its text to --color-content-default,
which is --color-gray-12 — the same token --color-background-default-knockout
paints a dark app bar with. Gray-12 on gray-12 is a contrast ratio of 1:1.
chrome takes its color from context instead, so the containing
App Bar decides it and the same markup works on a
dark bar and a light one. The hover scrim and focus ring derive from
currentColor for the same reason. This variant absorbed
esa-icon-button (with iconOnly) and esa-icon-link
(with a label) — both are deprecated shims now.
<EsaAppBar tone="brand-strong">
<EsaButton variant="chrome" icon="trees" iconRight="chevron-down">Mitigation work</EsaButton>
<EsaButton variant="chrome" current href="/reporting" icon="trending-up">Reporting</EsaButton>
<EsaButton slot="end" variant="chrome" iconOnly icon="search" label="Search" />
</EsaAppBar> API
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'ai' | 'ghost' | 'chrome' | 'primary' | Semantic intent. `ai` uses the copper AI-context palette; `ghost` is neutral/transparent. |
appearance | 'fill' | 'outline' | 'dashed' | 'soft' | 'fill' | How the variant renders — solid fill, bordered, dashed border, or a soft neutral surface with accent-colored text. |
active | boolean | false | Pressed/selected state (sets aria-pressed) — for toggle buttons. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Control size (shared form scale). |
disabled | boolean | false | Disables interaction. |
loading | boolean | false | Shows a spinner; label is held in place but hidden. |
icon | string | '' | Leading Lucide icon name, rendered left of the label. |
iconRight | string | '' | Lucide icon shown to the right of the label — the affordance (`arrow-right`, `chevron-down`). Independent of `icon`; a button may carry both. |
iconOnly | boolean | false | Square icon-only button with no text. Uses `icon` and ignores `iconRight`. Requires `label`. |
label | string | '' | Accessible name. Required with `iconOnly` — there is no text for a screen reader otherwise. Also sets `title`, giving the hover tooltip icon-only chrome needs. |
current | boolean | false | Marks the current item in a navigation set (`aria-current`). Distinct from `active`, which is a toggle's pressed state (`aria-pressed`) — a nav link announced as a depressed toggle is worse than an unlabelled one. |
type | 'button' | 'submit' | 'reset' | 'button' | Native button type. |
tag | 'a' | 'button' | 'summary' | — | Override the rendered element. `href` already picks `<a>` vs `<button>`; this covers the case inference cannot reach — `summary`, the disclosure trigger inside `<details>`. |
href | string | — | When set, the button renders as an <a> styled identically — a link that looks like a button. |
target | string | — | Link target (href only), e.g. "_blank". |
rel | string | — | Link rel (href only). Defaults to "noreferrer" when target="_blank". |
class | string | '' | Extra class(es) — applied to the styled wrapper, not the native control. |
color | 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'ai' | 'ghost' | 'chrome' | — | DEPRECATED — renamed to `variant`. Still honoured; warns at build time. |
Any other attribute (onclick, id, name,
data-*, aria-*) is forwarded verbatim to the native
element, so plain-JS callers can wire a handler or grab it by id.
Accessibility
- Renders a native
<button>— keyboard and screen-reader behavior come for free. - Visible focus ring via
--focus-ring-*tokens (2px, offset). activesetsaria-pressed(a toggle);currentsetsaria-current(a nav position). They are separate props because they are separate claims.iconOnlyrequireslabel. This is checked at build time and warns rather than failing the type — the label is a slot, so a boolean flag cannot make a sibling prop mandatory. Passingaria-labeldirectly also satisfies it.variant="chrome"derives its focus ring fromcurrentColorrather than--focus-ring-color, so the ring stays visible on a dark app bar.loadingkeeps the label in the DOM (visually hidden) so width doesn't jump and the name persists.warningfill uses dark text for contrast on the amber background.