Skip to content

HyButton ​

Usage examples on this page are written for Lit / plain HTML (<hy-button>). The same component ships as HyButton in @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 on href property
  • Five-value emphasis axis (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-live regions
  • 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 prefix or suffix and 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: set aria-label. A button whose label is always a glyph is hy-icon-button.
  • On a button that opens a menu, popover or disclosure, set aria-haspopup and keep aria-expanded in 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-disabled with 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. Keep href on 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 an hy-toggle-group for 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 ​

html
<hy-button>Click Me</hy-button>

Primary action button with enhanced styling ​

html
<hy-button variant="brand" size="large" emphasis="soft"> Primary Action </hy-button>
html
<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 ​

html
<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) ​

html
<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>
html
<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 ​

html
<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 ​

html
<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 ​

css
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) ​

html
<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 ​

typescript
// 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 ​

PropertyAttributeTypeDefaultDescription
variantvariant'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'highlight''neutral'Visual style variant determining the button's color scheme and semantic meaning.
sizesize'small' | 'medium' | 'large''medium'Physical size of the button affecting dimensions, padding, and typography scale.
emphasisemphasis'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.
shapeshape'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
hrefhrefstring | undefinedundefinedURL destination for link-mode buttons. When provided, the button renders as an anchor element.
targettargetstring | undefinedundefinedTarget window/frame for link navigation. Only effective when href is specified.
relrelstring | undefinedundefinedLink relationship descriptor for link-mode buttons. Only effective when href is specified.
disableddisabledbooleanfalseDisabled 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).
softDisabledsoft-disabledbooleanfalseSoft-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.
loadingloadingbooleanfalseLoading state providing visual feedback during asynchronous operations.
caretcaretbooleanfalseDisplays a dropdown caret icon after the button content.
autofocusautofocusbooleanfalseMarks 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.
truncatetruncatebooleanfalseTruncate 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.
fullWidthfull-widthbooleanfalseStretch 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.
typetype'button' | 'submit' | 'reset''button'HTML button type for form integration. Only applies to button-mode (non-link) buttons.
formformstring | 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.
ariaLabelaria-labelstring | nullnullAccessible label override for screen readers when visible text is insufficient.
ariaDescribedbyaria-describedbystring | nullnullReferences to elements providing additional descriptive information.
ariaExpandedaria-expandedstring | nullnullIndicates the expanded/collapsed state of collapsible content controlled by this button.
ariaHaspopuparia-haspopupstring | nullnullIndicates the type of popup element triggered by this button.
loadingTextloading-textstring'Loading'Screen reader announcement text for loading states.

Events ​

EventDetailDescription
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 ​

SlotDescription
defaultPrimary button content (text, inline elements). Usage: Main button label; can contain text nodes and inline elements. Accessibility: Content contributes to accessible name calculation.
prefixContent displayed before the main button text. Usage: Icons, badges, or decorative elements that precede the label. Layout: Positioned with appropriate spacing from main content.
suffixContent 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 ​

PartDescription
baseThe 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.

PropertyDefaultDescription
--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-smvar(--hy-gap-form-sm)Gap between button content elements
--hy-button-radius-action-md-top-startvar(--hy-radius-action-md-top-start)Corner rounding, top start (logical) — all sizes
--hy-button-radius-action-md-top-endvar(--hy-radius-action-md-top-end)Corner rounding, top end (logical) — all sizes
--hy-button-radius-action-md-bottom-startvar(--hy-radius-action-md-bottom-start)Corner rounding, bottom start (logical) — all sizes
--hy-button-radius-action-md-bottom-endvar(--hy-radius-action-md-bottom-end)Corner rounding, bottom end (logical) — all sizes
--hy-button-radius-pill-mdvar(--hy-radius-pill-md)The corner of a shape="round" button (the pill rung)
--hy-button-padding-form-smvar(--hy-padding-form-sm)Horizontal padding for small size variant
--hy-button-padding-form-mdvar(--hy-padding-form-md)Horizontal padding for medium size variant
--hy-button-padding-form-lgvar(--hy-padding-form-lg)Horizontal padding for large size variant
--hy-button-background-brand-solid-restvar(--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-smvar(--hy-stroke-sm)Border width for outlined emphasis
--hy-button-text-transformnoneText transform for button label Default: none
--hy-button-min-widthautoMinimum width of the button
--hy-button-control-height-smvar(--hy-control-height-sm)Outer height for small size variant. Per-component override in the form-control height contract.
--hy-button-control-height-mdvar(--hy-control-height-md)Outer height for medium size variant.
--hy-button-control-height-lgvar(--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 slot attribute)
  • 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
typescript
// 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');

Built with Lit. Documented with VitePress.