Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Button

A button lets people trigger an action, such as submitting a form, saving changes, or opening a dialog. Providing a destination turns it into a link instead, so the same element can also be used for navigation. Its content can be a text label, an icon, or a combination of both, and it can show a loading state while an action is in progress.

Dependencies

eun-icon · if using an icon eun-loader · if loading
Overview API Examples Accessibility
Confirm Cancel Delete Disabled

When to use

Use a button to trigger an action that happens right there on the page, such as submitting a form, saving changes, opening a dialog, or deleting something. It suits any situation where people expect an immediate, in-page response as soon as they activate it. If activating it should instead take someone to a different page, see the alternatives below.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/button.js";
<eun-button variant="primary">Confirm</eun-button>
<eun-button variant="secondary">Cancel</eun-button>

Importing the file registers <eun-button> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want .. Prefers A text-styled link for page navigation eun-link

Guidance

  • Use one primary button per section, for the main action
  • Use secondary for cancel/back actions alongside it
  • Reserve critical for destructive actions (delete, remove)
  • Use warning for an action that deserves caution but isn't destructive (overwrite, proceed anyway)
  • Start labels with an action verb ("Save changes", not "Changes")
  • Set loading during an async action to prevent double-submits
  • Wire it to a target with command/commandfor instead of a click handler whenever the button doesn't need to run other logic first, preferred over the Popover API's own popovertarget: see Invoker Commands API below
  • Stacking several primary buttons side by side
  • Using eun-button for plain text navigation instead of eun-link (see Alternatives above), unless href is set for a link styled as a button
  • Overriding colors with inline styles instead of the --button-* CSS variables
  • Disabling a button without explaining why elsewhere in the UI

Live testing

Properties

Attributes

NameTypeDefaultDescription
aria-labelstringThe accessible label for the button. When the button opens a link in a new tab, this automatically gets a note appended announcing it
loading-labelstring'Loading…'The accessible label announced while the button is loading
iconEunomiaIconNameAn optional leading icon, shown at the start of the button. Ignored once something is slotted into start, and overridden by the default icon from severity when both are set
severity'info' | 'success' | 'warning' | 'critical' | 'neutral'The severity of the action this button leads to. When set, it supplies a default leading icon unless icon is also set or something is slotted into start. It has no effect on color, which is still controlled by variant
appearance'default' | 'fill' | 'outline' | 'flat' | 'raised' | 'ghost''fill'The button's surface style. Fill is a solid, colored background with light text. Default is a paler tint of the same color. Outline keeps the color as a border instead of a fill. Flat always renders a muted, low emphasis surface regardless of variant. Raised adds a drop shadow. Ghost shows no surface at rest, only gaining a border and a tinted background on hover, focus, or press
fullbooleanfalseWhether the button stretches to fill 100% of the width and height available from its parent, instead of sizing to its content. Its content stays centered on both axes once stretched
activebooleanfalseIndicates whether the component is on disabled state or not. By default, set to false.

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
typestring'button'The button's native type. Ignored when href is set
variant'primary' | 'secondary' | 'critical' | 'success' | 'warning' | 'light' | 'medium' | 'dark''primary'The color theme applied to the button. light/medium/dark are neutral grey tones with no semantic meaning, matching eun-tag's color scale, for a de-emphasized action next to a semantically-colored one
sizestring'm'The size of the button
roundedbooleanfalseWhether the button has fully rounded corners
slidebooleanfalseWhether the button uses a sliding open animation
hrefstringThe URL the button navigates to. When set, the button renders as a link instead of a button
target'_blank' | '_self' | '_parent' | '_top'Where to open the linked URL. Only used when href is set. Opening in a new tab automatically adds an external link icon and announces it to assistive technology
downloadstringThe filename used when the linked URL is downloaded instead of navigated to. Only used when href is set
loadingbooleanfalseWhether the button shows a loading indicator. Like disabled, it blocks interaction, but keeps the original content in place so the button doesn't change size
commandstringThe command sent to the element targeted by commandFor when the button is clicked, following the Invoker Commands API
commandForstringThe id of the element that command is sent to

Slots

NameDescription
(default)The button content
startAn icon or other content shown at the start of the button. Takes priority over the default icon from icon or severity. Leave empty to let one of those render instead
endAn icon or other content shown at the end of the button. Ignored when href and target are set to open in a new tab, since an external link indicator takes its place

Events

NameTypeDescription
clickEventFired when the button is activated by mouse, Enter, or Space
eunnavigateNavigationEventFired before following href, so a client-side router can intercept the navigation. Not fired for downloads, and cancelable

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--button-backgroundSets the background color
--button-background-hoverSets the background color on hover
--button-background-activeSets the background color while pressed
--button-textSets the text color
--button-paddingSets the button's padding
--button-border-radiusSets the corner radius of the button
--button-focus-outlineSets the color of the focus outline
--button-transitionSets the duration of the button's transitions
--button-shadowSets a box shadow. None is applied by default
--button-shadow-hoverSets the box shadow on hover, falling back to button-shadow when unset
--button-border-colorSets the border color, overriding the color the active variant would otherwise use
--button-border-color-hoverSets the border color on hover or focus, falling back to button-border-color when unset
--button-border-sizeSets the width of the border

Variants

variant selects the color role: primary (default), secondary, critical, success, or warning.

Primary Secondary Critical Success Warning
<eun-button>Primary</eun-button>
<eun-button variant="secondary">Secondary</eun-button>
<eun-button variant="critical">Critical</eun-button>
<eun-button variant="success">Success</eun-button>
<eun-button variant="warning">Warning</eun-button>

Part of the shared vocabulary covered in Variants, alongside every other component that reuses it.

Neutral variants

light, medium, and dark add no semantic meaning of their own. They use the same grey scale Tag's color uses. Reach for one when an action needs to sit next to a semantically-colored button without competing with it, such as a secondary "Learn more" alongside a primary CTA.

Confirm Learn more Learn more Learn more
<eun-button>Confirm</eun-button>
<eun-button variant="light">Learn more</eun-button>

Invoker Commands API

Setting command and commandfor turns a button into an invoker: no click handler needed, the target picks up the command itself. Prefer this over wiring a click handler yourself, and over the Popover API's own popovertarget, which only ever covers popover-attributed targets. Modal and Drawer both understand show-modal, close, and request-close out of the box.

This will permanently remove the item from your workspace.

Delete item
<eun-modal
  id="delete-modal"
  heading="Delete item"
  confirm-label="Delete"
  confirm-variant="critical"
>
  <p>This will permanently remove the item from your workspace.</p>
</eun-modal>
<eun-button command="show-modal" commandfor="delete-modal"
  >Delete item</eun-button
>

A command name starting with -- is a custom command: nothing happens by default, so the target listens for it and decides what it means.

<eun-button command="--print" commandfor="invoice">Print</eun-button>
document.getElementById("invoice").addEventListener("command", (event) => {
  if (event.command === "--print") {
    window.print();
  }
});

See the Invoker Commands API guide for how the attributes work and current browser support.

Appearances

appearance picks the surface treatment, independent of variant. It uses the same vocabulary alert and other feedback components use for default/fill/outline/flat, extended with two action-only treatments, raised (a drop shadow) and ghost (no visible surface at all until hover/focus/active). flat always renders the same muted, low-emphasis surface regardless of variant, meant for low-priority actions rather than reinforcing the brand color. ghost is similar, but starts completely invisible, only appearing on hover/focus/active when no variant is set (set one to color it instead). It's a good fit for a secondary/dismiss action that shouldn't visually compete with a primary one nearby, e.g. eun-modal's own default cancel button.

Fill Default Outline Raised Flat Ghost

fill is the default look when appearance is left unset, a solid, opaque variant-colored background. ghost has no background or border at rest by design. Hover/focus it above to see it appear. The dashed outline is added for this demo only, so its hit area stays visible at rest, and isn't part of the real component (see the plain markup below).

<eun-button appearance="fill">Fill</eun-button>
<eun-button appearance="default">Default</eun-button>
<eun-button appearance="outline">Outline</eun-button>
<eun-button appearance="raised">Raised</eun-button>
<eun-button appearance="flat">Flat</eun-button>
<eun-button appearance="ghost">Ghost</eun-button>

Sizes

Five sizes are available through the size attribute: xs, s, m (default), l, xl.

Extra small Small Medium Large Extra large
<eun-button size="xs">Extra small</eun-button>
<eun-button size="l">Large</eun-button>

Rounded

The boolean rounded attribute gives the button a pill shape.

Rounded
<eun-button rounded>Rounded</eun-button>

Full width/height

The boolean full attribute stretches the button to 100% of the width and height its own parent makes available, instead of sizing to its own content. It's handy for two buttons side by side, one with an icon and one without, that should share the exact same height. Content stays centered on both axes regardless of how much extra space that adds. Give each button its own sized wrapper (here, an equal flex: 1 column) rather than placing several full buttons directly as siblings in one shared row, since each would then try to claim the entire row for itself.

Save
Cancel
<div style="display: flex; gap: 8px; height: 80px;">
  <div style="flex: 1;">
    <eun-button full>Save</eun-button>
  </div>
  <div style="flex: 1;">
    <eun-button full variant="light">
      <eun-icon slot="start" name="close"></eun-icon>
      Cancel
    </eun-button>
  </div>
</div>

With icons

The leading (start) icon can be set either through the icon property, or by slotting a eun-icon (or any content) into start, a manual override for anything the icon prop's plain string can't express. Both produce the same result for a plain icon. Reach for the slot as soon as it needs to be something else (e.g. combined with slide, below). The trailing (end) icon has no property equivalent, so always slot it. Setting only an icon in the default slot produces an icon-only button, so take care to apply the accessibility rules in the Accessibility tab when doing so.

Icon prop Slotted icon Right icon
import "@eunomia/elements/icon.js";
<eun-button icon="iconName">Label</eun-button>

<!-- equivalent, spelled out by hand -->
<eun-button>
  <eun-icon slot="start" name="iconName"></eun-icon>
  Label
</eun-button>

The icon module must be imported alongside the button, exactly like eun-loader below for the loading state.

Severity

severity (info, success, warning, critical, or neutral, the same vocabulary as Alert/Tag) sets the button's leading icon by default, once set and icon isn't (and nothing is slotted into start). It has no effect on color, which stays driven by variant instead. A full severity range doesn't have a matching action color (no "info"/"neutral" button), so the two stay independent rather than one implying the other.

Info Success Warning Critical Neutral
<eun-button severity="info">Info</eun-button>
<eun-button severity="success">Success</eun-button>
<eun-button severity="warning">Warning</eun-button>
<eun-button severity="critical">Critical</eun-button>
<eun-button severity="neutral">Neutral</eun-button>

An explicit icon (prop or slotted) always takes precedence over severity's own default.

Icon-only

Slot just an icon with no visible label, and always pair it with an aria-label describing the action. The icon itself carries no accessible text (it's decorative, rendered aria-hidden), so without one the button would announce nothing at all to a screen reader.

<eun-button aria-label="Delete" variant="critical">
  <eun-icon name="delete"></eun-icon>
</eun-button>

Slide button

It shows the label on hover or focus and collapses to just the icon otherwise, using only an icon slotted into start. It's implemented in pure CSS, with no measurement and no JavaScript needed. The label's grid track animates from 0fr to 1fr, so it always expands to exactly its own natural content width, however long that is, with no hardcoded width to tune.

Remove
<eun-button slide variant="critical">
  <eun-icon slot="start" name="delete"></eun-icon>
  Remove
</eun-button>

Loading

Setting loading blocks interaction the same way disabled does. The button keeps its original content in place, kept invisible so its size doesn't change, while a eun-loader spinner is layered on top.

Save changes
import "@eunomia/elements/loader.js";
<eun-button loading>Save changes</eun-button>

eun-loader is used internally, but its module must still be imported alongside eun-button for the spinner to render, exactly like eun-icon for slotted icons above.

Links

Setting href renders the button as a native <a> internally instead of a <button>, delegating navigation, focus, and activation entirely to the browser. All the same visual props (variant, size, rounded, ...) still apply.

Visit website
<eun-button href="https://example.com">Visit website</eun-button>

When href is set, type is ignored (an <a> has no submit/reset mechanism) and the host is removed from the tab order, since the rendered <a> is the real, natively focusable element.

Opening in a new tab

Setting target="_blank" automatically adds rel="noopener noreferrer", replaces the end slot with an external-link icon, and appends "(opens in a new tab)" to the accessible name.

Visit website
<eun-button href="https://example.com" target="_blank">
  Visit website
</eun-button>

Disabled link

Native <a> elements have no disabled attribute, so a disabled link button simply omits href instead. It keeps its usual visual style, but is no longer exposed as a link to the browser or to assistive technology, and gets aria-disabled="true" set for clarity.

Visit website
<eun-button href="https://example.com" disabled> Visit website </eun-button>

SPA navigation

A link button dispatches a cancelable eunnavigate event right before following href, letting an SPA router intercept it for client-side routing instead of a full page load. It's never dispatched for download links, since a file download isn't a navigation.

document.addEventListener("eunnavigate", (event) => {
  if (!event.href.startsWith("/")) {
    return;
  }
  event.preventDefault();
  myRouter.push(event.href);
});

Theming

Colors come from the active theme's --eun-color-primary-* / --eun-color-secondary-* / --eun-color-critical-* / --eun-color-success-* / --eun-color-warning-* tokens (see Theming). Switch the toolbar above and every demo on this page repaints immediately. For one-off overrides, target the component's own --button-* custom properties instead of the theme tokens directly (full list in the API tab).

Custom button
<eun-button
  style="
    --button-background: #b45309;
    --button-background-hover: #92400e;
    --button-focus-outline: #b45309;
  "
>
  Custom button
</eun-button>

Keyboard interactions

When focus is on the component:

Key Action
Enter Activates the button
Space Activates the button

Focus indicator

A :focus-visible outline is always rendered via outline, not a background/shadow change, so it isn't suppressed by any surface appearance (raised, outline, flat…). Its color comes from a shade picked specifically for contrast against the page background, one step darker than the button's own fill rather than reusing the fill color itself, so the ring stays visible instead of blending into the button on secondary and other lighter variants. Override it with --button-focus-outline if a custom theme needs a different color.

Aria attributes

  • aria-label / aria-labelledby: set when the visible label isn't sufficient on its own
  • aria-haspopup="true": when the button controls a menu, listbox, modal, or anything using the popover API
  • aria-expanded="true\|false": for buttons controlling an accordion or disclosure
  • aria-pressed="true\|false": for toggle buttons
  • For a link button opening a new tab, "(opens in a new tab)" is announced automatically, so no manual wording is needed
  • While loading, aria-busy="true" is set and the loading indicator exposes role="status" with an accessible label (loadingLabel, defaults to "Loading…")

Label accessibility

The accessible name is resolved in order of priority: aria-labelledby, then aria-label, then the slotted text content, then title as a last resort. When using aria-labelledby or aria-label, include the visible label within the accessible one, since voice-command users rely on that match.

Icon-only buttons

Add an aria-label repeating the intended action, since the icon alone isn't announced. The title attribute doesn't appear on keyboard navigation and doesn't scale with page zoom, so limit icon-only buttons to recurring, easily identifiable actions. See the Icon-only example in the Examples tab for the exact pattern.

Disabled state

A disabled button is not focusable and shouldn't carry a tooltip or other pointer-only affordance, since a keyboard user could never reach it to trigger that affordance in the first place. Avoid disabling a form's submit button. Prefer inline validation messaging instead, so keyboard and screen-reader users understand why the action is currently blocked rather than being met with an unreachable, unexplained control.

Slide button

The label stays in the accessible name at all times, even while visually collapsed to just the icon. It's hidden with opacity, which affects paint only, not the accessibility tree. A screen reader always announces the full label (e.g. "Remove"), regardless of whether the sighted, hover-revealed state is currently showing.