HyButton
Usage examples on this page are written for Lit / plain HTML (
<hy-button>). The same component ships asHyButtonin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
A versatile, accessible button component supporting multiple variants, sizes, and visual appearances.
Key Features:
- Dual-mode rendering: Functions as both buttons (
<button>) and links (<a>) based onhrefproperty - Five-value
emphasisaxis (solid | soft | outlined | tinted | plain) — single-token, no compounds - Comprehensive accessibility support including ARIA attributes and keyboard navigation
- Loading states with visual feedback and screen reader announcements
- Flexible content model with prefix, suffix, and default slots
- Built-in popup/dropdown support with escape key handling
- CSS custom properties for extensive theming capabilities
Accessibility: built in
- Full keyboard navigation support (Enter, Space, Escape keys)
- Proper ARIA attributes for screen readers
- Loading state announcements via
aria-liveregions - Focus management for disabled and loading states
- Semantic role consistency between button and link modes
Accessibility: your part
- A glyph-only button — a glyph in
prefixorsuffixand nothing in the default slot, the usual shape when the label comes and goes with a state (a count that is sometimes zero) — is square and has no text to be named by: setaria-label. A button whose label is always a glyph ishy-icon-button. - On a button that opens a menu, popover or disclosure, set
aria-haspopupand keeparia-expandedin step with the open state.
Performance Considerations:
- Lightweight DOM structure with minimal nesting
- Efficient event handling with proper cleanup
- CSS-only animations and transitions
- No unnecessary re-renders during property changes
A main action with more options beside it
Use hy-split-button (Save, then Save as… behind a chevron) rather than a button next to a menu button; for a menu of actions with no default, hy-menu-button.
When to use
- An action that happens right away on this page: Save, Send, Delete, Add.
- Submitting or resetting a form.
- Opening a dialog, a drawer or a menu that asks for more.
- A navigation that must stand out as the page's main action, with
href.
Do
- Name the action in the label, not "OK" or "Yes".
- Give one action per area the solid emphasis and the others a quieter one.
- Use
soft-disabledwith a reason people can reach when an action is unavailable for now.
When not to use
- Activation only takes people to another page:
hy-link. Keephrefon a button for a navigation that deserves button prominence. - A button whose whole content is one icon:
hy-icon-button. - An option that stays pressed:
hy-toggle, in anhy-toggle-groupfor a set. A setting that applies at once:hy-switch. variant="danger"is for destructive actions only.variant="highlight"is for the one action that stands out on a screen: at most one, never a routine or destructive action.- Waiting on work the button started:
loading. Do not disable or swap the button.
Examples
Basic usage - Default neutral button
<hy-button>Click Me</hy-button>Primary action button with enhanced styling
<hy-button variant="brand" size="large" emphasis="soft"> Primary Action </hy-button>Link button with external navigation
<hy-button
href="https://example.com"
target="_blank"
rel="noopener noreferrer"
variant="success"
emphasis="outlined"
>
Visit External Site
</hy-button>Loading state with custom announcement
<hy-button loading loading-text="Saving your changes...">
<hy-icon slot="prefix" name="download"></hy-icon>
Save Document
</hy-button>Soft-disabled (tooltip explaining why stays reachable)
<hy-tooltip>
<hy-button slot="trigger" emphasis="outlined" soft-disabled> Continue with Google </hy-button>
Google sign-in is coming soon — use your email and password for now.
</hy-tooltip>Dropdown trigger with accessibility features
<hy-button
caret
variant="neutral"
emphasis="plain"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="dropdown-menu"
>
More Options
<hy-icon slot="suffix" name="settings"></hy-icon>
</hy-button>Form submission with validation
<hy-button type="submit" variant="brand" aria-describedby="submit-help"> Submit Form </hy-button>
<div id="submit-help">Review your information before submitting</div>Advanced accessibility configuration
<hy-button
aria-label="Open navigation menu"
aria-expanded="false"
aria-haspopup="menu"
aria-describedby="nav-help"
@click="${handleMenuToggle}"
@escape="${handleMenuClose}"
>
<hy-icon name="menu"></hy-icon>
</hy-button>
<div id="nav-help">Use arrow keys to navigate menu items</div>Theming via CSS custom properties
hy-button {
--hy-button-radius-action-md-top-start: var(--hy-radius-form-lg-top-start);
--hy-button-radius-action-md-top-end: var(--hy-radius-form-lg-top-end);
--hy-button-radius-action-md-bottom-start: var(--hy-radius-form-lg-bottom-start);
--hy-button-radius-action-md-bottom-end: var(--hy-radius-form-lg-bottom-end);
--hy-button-text-transform: uppercase;
}Single-token emphasis (no compounds — see no-compounds rule)
<hy-button emphasis="solid">Solid (default)</hy-button>
<hy-button emphasis="soft">Soft</hy-button>
<hy-button emphasis="outlined">Outlined</hy-button>
<hy-button emphasis="tinted">Tinted</hy-button>
<hy-button emphasis="plain">Plain</hy-button>Event handling with proper typing
// Native click bubbles from the internal element — gated when disabled/loading.
button.addEventListener('click', (event) => {
console.log('Button clicked:', event);
});
button.addEventListener('escape', (event: CustomEvent) => {
const { originalEvent } = event.detail;
closeAssociatedPopup();
});Click gating: Native click events bubble normally. When disabled or loading, clicks are stopped (stopPropagation + preventDefault) on the host so external listeners never see them.
API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
variant | variant | 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'highlight' | 'neutral' | Visual style variant determining the button's color scheme and semantic meaning. |
size | size | 'small' | 'medium' | 'large' | 'medium' | Physical size of the button affecting dimensions, padding, and typography scale. |
emphasis | emphasis | 'solid' | 'soft' | 'outlined' | 'tinted' | 'plain' | 'solid' | Visual chrome weight on top of the role. Single-token enum — no compounds. Orthogonal to variant (role) and the state axis. |
shape | shape | 'square' | 'round' | 'square' | The button's corner — hy-icon-button's shape, the same two words. - square — the action corner (--hy-radius-action-md, per logical corner, so a container can square one side: the split button's inner seam) (default) - round — a pill (--hy-radius-pill-md), for a status pill that is pressed (a count that opens a menu, a retry) or a toolbar of rounded actions; Ant shape="round", Fluent shape="circular", Shoelace / Web Awesome pill |
href | href | string | undefined | undefined | URL destination for link-mode buttons. When provided, the button renders as an anchor element. |
target | target | string | undefined | undefined | Target window/frame for link navigation. Only effective when href is specified. |
rel | rel | string | undefined | undefined | Link relationship descriptor for link-mode buttons. Only effective when href is specified. |
disabled | disabled | boolean | false | Disabled state preventing all user interaction and form submission. The button drops its role: solid, soft, tinted and outlined gray the box they already have; plain stays boxless (muted text only). |
softDisabled | soft-disabled | boolean | false | Soft-disables the button: interaction is blocked and disabled styling applies, but the button stays focusable and hoverable. Use instead of disabled when the button carries an explanation (e.g. a tooltip saying why it's unavailable) that keyboard and screen-reader users must be able to reach. Exposed as aria-disabled="true"; native disabled is NOT set. |
loading | loading | boolean | false | Loading state providing visual feedback during asynchronous operations. |
caret | caret | boolean | false | Displays a dropdown caret icon after the button content. |
autofocus | autofocus | boolean | false | Marks this button as the auto-focus target. Read by hy-dialog (and other overlay containers) on show to choose initial focus — useful for the primary action in a confirmation dialog. Also focuses on first connect when used outside an overlay, mirroring native HTML autofocus but routed through JS so SSR'd HTML doesn't pull focus during hydration. |
truncate | truncate | boolean | false | Truncate overflowing label text with an ellipsis. When enabled, the button stretches to fill its container's width and the default-slot label clips with text-overflow: ellipsis once it no longer fits. |
fullWidth | full-width | boolean | false | Stretch the button to fill its container's inline size. Off by design — buttons are content-width so they sit naturally in toolbars and action rows; opt in for full-bleed contexts (mobile forms, stacked CTAs, a select-style trigger). Mirrors MUI fullWidth / Ant block. |
type | type | 'button' | 'submit' | 'reset' | 'button' | HTML button type for form integration. Only applies to button-mode (non-link) buttons. |
form | form | string | undefined | — | The id of the form a submit or reset button acts on when it sits outside it — a dialog footer, a toolbar — as a native button's form attribute. Unset, the button acts on the form it is inside. |
ariaLabel | aria-label | string | null | null | Accessible label override for screen readers when visible text is insufficient. |
ariaDescribedby | aria-describedby | string | null | null | References to elements providing additional descriptive information. |
ariaExpanded | aria-expanded | string | null | null | Indicates the expanded/collapsed state of collapsible content controlled by this button. |
ariaHaspopup | aria-haspopup | string | null | null | Indicates the type of popup element triggered by this button. |
loadingText | loading-text | string | 'Loading' | Screen reader announcement text for loading states. |
Events
| Event | Detail | Description |
|---|---|---|
escape | — | Emitted when Escape key is pressed on buttons with aria-haspopup. Detail: {originalEvent: KeyboardEvent} - Contains the original keyboard event. Usage: Ideal for closing associated popups, menus, or dialogs. Condition: Only fired when aria-haspopup attribute is present. |
Slots
| Slot | Description |
|---|---|
default | Primary button content (text, inline elements). Usage: Main button label; can contain text nodes and inline elements. Accessibility: Content contributes to accessible name calculation. |
prefix | Content displayed before the main button text. Usage: Icons, badges, or decorative elements that precede the label. Layout: Positioned with appropriate spacing from main content. |
suffix | Content displayed after the main button text. Usage: Icons, badges, or decorative elements that follow the label. Note: Appears before the caret icon when both are present. |
CSS Parts
| Part | Description |
|---|---|
base | The root button or anchor element. Usage: Primary styling target for customizing the button's appearance. Element Type: Either <button> or <a> depending on href property. |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-button-background-image-size | — | The background image's size (the wash, a highlight gradient); a container's to set (default auto) |
--hy-button-background-image-position | — | Where that image sits (default 0 0) |
--hy-button-gap-form-sm | var(--hy-gap-form-sm) | Gap between button content elements |
--hy-button-radius-action-md-top-start | var(--hy-radius-action-md-top-start) | Corner rounding, top start (logical) — all sizes |
--hy-button-radius-action-md-top-end | var(--hy-radius-action-md-top-end) | Corner rounding, top end (logical) — all sizes |
--hy-button-radius-action-md-bottom-start | var(--hy-radius-action-md-bottom-start) | Corner rounding, bottom start (logical) — all sizes |
--hy-button-radius-action-md-bottom-end | var(--hy-radius-action-md-bottom-end) | Corner rounding, bottom end (logical) — all sizes |
--hy-button-radius-pill-md | var(--hy-radius-pill-md) | The corner of a shape="round" button (the pill rung) |
--hy-button-padding-form-sm | var(--hy-padding-form-sm) | Horizontal padding for small size variant |
--hy-button-padding-form-md | var(--hy-padding-form-md) | Horizontal padding for medium size variant |
--hy-button-padding-form-lg | var(--hy-padding-form-lg) | Horizontal padding for large size variant |
--hy-button-background-brand-solid-rest | var(--hy-background-brand-solid-rest) | One of the generated color hooks (paint/button.mjs): --hy-button[-<part>]-<family>-<variant>-<emphasis-or-on>-<state> (family: background, foreground or border), one per cell and state, each falling back to the job token of the same name (the default cell is neutral solid). Components ▸ Add an override lists them all. |
--hy-button-stroke-sm | var(--hy-stroke-sm) | Border width for outlined emphasis |
--hy-button-text-transform | none | Text transform for button label Default: none |
--hy-button-min-width | auto | Minimum width of the button |
--hy-button-control-height-sm | var(--hy-control-height-sm) | Outer height for small size variant. Per-component override in the form-control height contract. |
--hy-button-control-height-md | var(--hy-control-height-md) | Outer height for medium size variant. |
--hy-button-control-height-lg | var(--hy-control-height-lg) | Outer height for large size variant. |
Methods
focus()
Sets focus on the underlying button or link element.
setTabIndex()
Projects a tabindex onto the inner button — called by parent roving-tabindex controllers so composite widgets (e.g. the markdown-editor's format toolbar) can keep a single tab stop.
blur()
Removes focus from the underlying button or link element.
setDefaultSlotText()
Programmatically updates the button's default slot text content.
Implementation Details:
- Removes existing default slot content (text nodes and unslotted elements)
- Preserves slotted content (elements with
slotattribute) - Adds new text as a text node to maintain semantic structure
- Triggers reactive update cycle for proper re-rendering
Use Cases:
- Dynamic button text based on application state
- Internationalization and language switching
- Context-sensitive button labeling
- Programmatic button configuration
Limitations:
- Only affects text content, not HTML structure
- Overrides any existing default slot content
- Does not affect slotted elements (prefix/suffix)
Alternative Approaches:
- For complex content, prefer declarative slot usage
- For reactive text, consider binding to slot content directly
- For HTML content, manipulate slot children instead
Parameters:
text- The new text content for the button's main label
// Dynamic button text
const button = document.querySelector('hy-button');
button.setDefaultSlotText('Save Changes');
// Internationalization
button.setDefaultSlotText(i18n.t('buttons.save'));
// State-based labeling
button.setDefaultSlotText(isEditing ? 'Save' : 'Edit');