Skip to content

HyButton

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...">
  <span slot="prefix">💾</span>
  Save Document
</hy-button>
html
<hy-button
  caret
  variant="neutral"
  emphasis="plain"
  aria-haspopup="menu"
  aria-expanded="false"
  aria-controls="dropdown-menu"
>
  More Options
  <span slot="suffix">⚙️</span>
</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-button>
<div id="nav-help">Use arrow keys to navigate menu items</div>

CSS Parts customization

css
hy-button::part(base) {
  border-radius: 12px;
  font-weight: 600;
  letter-spacing: 0.025em;
}

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


## Events

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

- **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.

## 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');

render()

Main render method producing the complete button component.

Rendering Strategy:

  • Link Mode (href provided + not disabled/loading): Renders as <a> element
  • Button Mode (default): Renders as <button> element

Element Selection Logic:

  • Links provide natural browser navigation and context menu support
  • Disabled/loading links fall back to button mode to prevent navigation
  • Both modes maintain consistent styling and behavior through CSS classes

Accessibility Implementation:

  • Links include role="button" for semantic consistency
  • Both modes receive identical ARIA attributes and keyboard handling
  • Focus management remains consistent across rendering modes
  • Screen readers receive appropriate element semantics

Attribute Management:

  • Dynamic attributes use ifDefined() directive for clean output
  • Boolean attributes are properly handled for both element types
  • ARIA attributes are consistently applied across both modes

Event Binding:

  • Both modes use identical event handlers for consistent behavior
  • Link mode maintains keyboard accessibility through custom handlers
  • Form integration works naturally with button elements

Built with Lit. Documented with VitePress.