Toggle Button
A two-state button that can be either "pressed" or "not pressed".
Demo
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
ToggleButton.astro
---
/**
* 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
Example
---
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.