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 Highlights:

  • 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

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

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="save"></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: var(--hy-radius-form-lg);
  --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

css
// 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


## API

### Properties

| Property | Attribute | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `variant` | `variant` | `'neutral' \| 'brand' \| 'success' \| 'warning' \| 'danger' \| 'info'` | `'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. |
| `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. |
| `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. |
| `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. |
| `caret-icon` | Custom caret icon (overrides the default chevron). **Condition:** Only rendered when the `caret` property is set. |

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

| Property | Description |
| --- | --- |
| `--hy-button-gap` | Gap between button content elements **Default:** `var(--hy-gap-form-sm)` |
| `--hy-button-radius` | Corner rounding for all button sizes **Default:** `var(--hy-radius-form-md)` |
| `--hy-button-padding-size-sm` | Horizontal padding for small size variant **Default:** `var(--hy-padding-form-sm)` |
| `--hy-button-padding-size-md` | Horizontal padding for medium size variant **Default:** `var(--hy-padding-form-md)` |
| `--hy-button-padding-size-lg` | Horizontal padding for large size variant **Default:** `var(--hy-padding-form-lg)` |
| `--hy-button-border-default-rest` | Border color for default variant **Default:** `var(--hy-border-default-rest)` |
| `--hy-button-border-brand-rest` | Border color for brand variant **Default:** `var(--hy-border-brand-rest)` |
| `--hy-button-border-success-rest` | Border color for success variant **Default:** `var(--hy-border-success-rest)` |
| `--hy-button-border-warning-rest` | Border color for warning variant **Default:** `var(--hy-border-warning-rest)` |
| `--hy-button-border-danger-rest` | Border color for danger variant **Default:** `var(--hy-border-danger-rest)` |
| `--hy-button-border-info-rest` | Border color for info variant **Default:** `var(--hy-border-info-rest)` |
| `--hy-button-stroke` | Border width for outlined appearance **Default:** `var(--hy-stroke-sm)` |
| `--hy-button-text-transform` | Text transform for button label **Default:** `none` |
| `--hy-button-font-family` | Font family override for button label |
| `--hy-button-min-width` | Minimum width of the button |
| `--hy-button-height-sm` | Outer height for small size variant. Per-component override in the form-control height contract (see `.claude/rules/control-heights.md`). **Default:** `var(--hy-control-height-sm)` |
| `--hy-button-height-md` | Outer height for medium size variant. **Default:** `var(--hy-control-height-md)` |
| `--hy-button-height-lg` | Outer height for large size variant. **Default:** `var(--hy-control-height-lg)` |

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

```javascript
// 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.