Toggle Button
A two-state button that can be either "pressed" or "not pressed".
🤖 AI Implementation GuideDemo
Accessibility Features
WAI-ARIA Roles
-
button- Indicates a widget that triggers an action when activated
WAI-ARIA button role (opens in new tab)
WAI-ARIA States
aria-pressed
Indicates the current pressed state of the toggle button.
| Values | true | false (tri-state buttons may also use "mixed") |
| Required | Yes (for toggle buttons) |
| Default | initialPressed prop (default: false) |
| Change Trigger | Click, Enter, Space |
| Reference | aria-pressed (opens in new tab) |
Keyboard Support
| Key | Action |
|---|---|
| Space | Toggle the button state |
| Enter | Toggle the button state |
Source Code
---
/**
* APG Toggle Button Pattern - Astro Implementation
*
* A two-state button that can be either "pressed" or "not pressed".
* Uses Web Components for client-side interactivity.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/button/
*/
export interface Props {
/** Initial pressed state */
initialPressed?: boolean;
/** Whether the button is disabled */
disabled?: boolean;
/** Additional CSS class */
class?: string;
}
const { initialPressed = false, disabled = false, class: className = '' } = Astro.props;
const stateClass = initialPressed ? 'apg-toggle-button--pressed' : 'apg-toggle-button--not-pressed';
const indicatorClass = initialPressed
? 'apg-toggle-indicator--pressed'
: 'apg-toggle-indicator--not-pressed';
// Check if custom indicator slots are provided
const hasPressedIndicator = Astro.slots.has('pressed-indicator');
const hasUnpressedIndicator = Astro.slots.has('unpressed-indicator');
const hasCustomIndicators = hasPressedIndicator || hasUnpressedIndicator;
---
<apg-toggle-button class={className}>
<button
type="button"
class={`apg-toggle-button ${stateClass}`}
aria-pressed={initialPressed}
disabled={disabled}
>
<span class="apg-toggle-button-content">
<slot />
</span>
<span
class={`apg-toggle-indicator ${indicatorClass}`}
aria-hidden="true"
data-custom-indicators={hasCustomIndicators ? 'true' : undefined}
>
{
hasCustomIndicators ? (
<>
<span class="apg-indicator-pressed" hidden={!initialPressed}>
<slot name="pressed-indicator">●</slot>
</span>
<span class="apg-indicator-unpressed" hidden={initialPressed}>
<slot name="unpressed-indicator">○</slot>
</span>
</>
) : initialPressed ? (
'●'
) : (
'○'
)
}
</span>
</button>
</apg-toggle-button>
<script>
class ApgToggleButton extends HTMLElement {
private button: HTMLButtonElement | null = null;
private rafId: number | null = null;
connectedCallback() {
// Use requestAnimationFrame to ensure DOM is fully constructed
this.rafId = requestAnimationFrame(() => this.initialize());
}
private initialize() {
this.rafId = null;
this.button = this.querySelector('button');
if (!this.button) {
console.warn('apg-toggle-button: button element not found');
return;
}
this.button.addEventListener('click', this.handleClick);
}
disconnectedCallback() {
// Cancel pending initialization
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
// Remove event listeners
this.button?.removeEventListener('click', this.handleClick);
this.button = null;
}
private handleClick = () => {
if (!this.button || this.button.disabled) return;
const currentPressed = this.button.getAttribute('aria-pressed') === 'true';
const newPressed = !currentPressed;
// Update aria-pressed
this.button.setAttribute('aria-pressed', String(newPressed));
// Update CSS classes
this.button.classList.toggle('apg-toggle-button--pressed', newPressed);
this.button.classList.toggle('apg-toggle-button--not-pressed', !newPressed);
// Update indicator
const indicator = this.button.querySelector('.apg-toggle-indicator');
if (indicator) {
const hasCustomIndicators = indicator.getAttribute('data-custom-indicators') === 'true';
if (hasCustomIndicators) {
// Toggle visibility of custom indicator slots
const pressedIndicator = indicator.querySelector('.apg-indicator-pressed');
const unpressedIndicator = indicator.querySelector('.apg-indicator-unpressed');
if (pressedIndicator instanceof HTMLElement) {
pressedIndicator.hidden = !newPressed;
}
if (unpressedIndicator instanceof HTMLElement) {
unpressedIndicator.hidden = newPressed;
}
} else {
// Use default text indicators
indicator.textContent = newPressed ? '●' : '○';
}
indicator.classList.toggle('apg-toggle-indicator--pressed', newPressed);
indicator.classList.toggle('apg-toggle-indicator--not-pressed', !newPressed);
}
// Dispatch custom event for external listeners
this.dispatchEvent(
new CustomEvent('toggle', {
detail: { pressed: newPressed },
bubbles: true,
})
);
};
}
// Register the custom element
if (!customElements.get('apg-toggle-button')) {
customElements.define('apg-toggle-button', ApgToggleButton);
}
</script> Usage
---
import ToggleButton from './ToggleButton.astro';
import Icon from './Icon.astro';
---
<ToggleButton>
<Icon name="volume-off" slot="pressed-indicator" />
<Icon name="volume-2" slot="unpressed-indicator" />
Mute
</ToggleButton>
<script>
// Listen for toggle events
document.querySelector('apg-toggle-button')?.addEventListener('toggle', (e) => {
console.log('Muted:', e.detail.pressed);
});
</script> API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
initialPressed | boolean | false | Initial pressed state |
disabled | boolean | false | Whether the button is disabled |
class | string | "" | Additional CSS class |
Slots
| Slot | Default | Description |
|---|---|---|
default | - | Button label content |
pressed-indicator | "●" | Custom indicator for pressed state |
unpressed-indicator | "○" | Custom indicator for unpressed state |
Custom Events
| Event | Detail | Description |
|---|---|---|
toggle | { pressed: boolean } | Fired when the toggle state changes |
This component uses a Web Component (<apg-toggle-button>) for client-side
interactivity.
Testing
Tests verify APG compliance across keyboard interaction, ARIA attributes, and accessibility requirements. The Toggle Button component uses a two-layer testing strategy.
Testing Strategy
Unit Tests (Testing Library)
Verify component rendering and interactions using framework-specific Testing Library utilities. These tests ensure correct component behavior in isolation.
- HTML structure and element hierarchy
- Initial attribute values (aria-pressed, type)
- Click event handling and state toggling
- CSS class application
E2E Tests (Playwright)
Verify component behavior in a real browser environment across all four frameworks. These tests cover interactions that require full browser context.
- Keyboard interactions (Space, Enter)
- aria-pressed state toggling
- Disabled state behavior
- Focus management and Tab navigation
- Cross-framework consistency
Test Categories
High Priority: APG Keyboard Interaction (E2E)
| Test | Description |
|---|---|
Space key toggles | Pressing Space toggles the button state |
Enter key toggles | Pressing Enter toggles the button state |
Tab navigation | Tab key moves focus between buttons |
Disabled Tab skip | Disabled buttons are skipped in Tab order |
High Priority: APG ARIA Attributes (E2E)
| Test | Description |
|---|---|
role="button" | Has implicit button role (via <button>) |
aria-pressed initial | Initial state is aria-pressed="false" |
aria-pressed toggle | Click changes aria-pressed to true |
type="button" | Explicit button type prevents form submission |
disabled state | Disabled buttons don't change state on click |
Medium Priority: Accessibility (E2E)
| Test | Description |
|---|---|
axe violations | No WCAG 2.1 AA violations (via jest-axe) |
accessible name | Button has an accessible name from content |
Low Priority: HTML Attribute Inheritance (Unit)
| Test | Description |
|---|---|
className merge | Custom classes are merged with component classes |
data-* attributes | Custom data attributes are passed through |
Testing Tools
- Vitest (opens in new tab) - Test runner for unit tests
- Testing Library (opens in new tab) - Framework-specific testing utilities (React, Vue, Svelte)
- Playwright (opens in new tab) - Browser automation for E2E tests
- axe-core/playwright (opens in new tab) - Automated accessibility testing in E2E
See testing-strategy.md (opens in new tab) for full documentation.
Resources
- WAI-ARIA APG: Button Pattern (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist