APG Patterns

Switch

A control that allows users to toggle between two states: on and off.

Demo

Accessibility Features

WAI-ARIA Roles

WAI-ARIA States

aria-checked

Indicates the current checked state of the switch.

Values true | false
Required Yes (for switch role)
Default initialChecked prop (default: false)
Change Trigger Click, Enter, Space
Reference aria-checked (opens in new tab)

aria-disabled

Indicates the switch is perceivable but disabled.

Values true | undefined
Required No (only when disabled)
Reference aria-disabled (opens in new tab)

Keyboard Support

Key Action
Space Toggle the switch state (on/off)
Enter Toggle the switch state (on/off)

Accessible Naming

Switches must have an accessible name. This can be provided through:

  • Visible label (recommended) - The switch's child content provides the accessible name
  • aria-label - Provides an invisible label for the switch
  • aria-labelledby - References an external element as the label

Visual Design

This implementation follows WCAG 1.4.1 (Use of Color) by not relying solely on color to indicate state:

  • Thumb position - Left = off, Right = on
  • Checkmark icon - Visible only when the switch is on
  • Forced colors mode - Uses system colors for accessibility in Windows High Contrast Mode

Source Code

Switch.astro
---
/**
 * APG Switch Pattern - Astro Implementation
 *
 * A control that allows users to toggle between two states: on and off.
 * Uses Web Components for client-side interactivity.
 *
 * @see https://www.w3.org/WAI/ARIA/apg/patterns/switch/
 */

export interface Props {
  /** Initial checked state */
  initialChecked?: boolean;
  /** Whether the switch is disabled */
  disabled?: boolean;
  /** Additional CSS class */
  class?: string;
}

const {
  initialChecked = false,
  disabled = false,
  class: className = "",
} = Astro.props;
---

<apg-switch class={className}>
  <button
    type="button"
    role="switch"
    class="apg-switch"
    aria-checked={initialChecked}
    aria-disabled={disabled || undefined}
    disabled={disabled}
  >
    <span class="apg-switch-track">
      <span class="apg-switch-icon" aria-hidden="true">
        <svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
          <path
            d="M10.28 2.28a.75.75 0 00-1.06-1.06L4.5 5.94 2.78 4.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.06 0l5.25-5.25z"
            fill="currentColor"
          />
        </svg>
      </span>
      <span class="apg-switch-thumb"></span>
    </span>
    {Astro.slots.has('default') && (
      <span class="apg-switch-label">
        <slot />
      </span>
    )}
  </button>
</apg-switch>

<script>
  class ApgSwitch extends HTMLElement {
    private button: HTMLButtonElement | null = null;
    private rafId: number | null = null;

    connectedCallback() {
      this.rafId = requestAnimationFrame(() => this.initialize());
    }

    private initialize() {
      this.rafId = null;
      this.button = this.querySelector('button[role="switch"]');
      if (!this.button) {
        console.warn("apg-switch: button element not found");
        return;
      }

      this.button.addEventListener("click", this.handleClick);
      this.button.addEventListener("keydown", this.handleKeyDown);
    }

    disconnectedCallback() {
      if (this.rafId !== null) {
        cancelAnimationFrame(this.rafId);
        this.rafId = null;
      }
      this.button?.removeEventListener("click", this.handleClick);
      this.button?.removeEventListener("keydown", this.handleKeyDown);
      this.button = null;
    }

    private toggle() {
      if (!this.button || this.button.disabled) return;

      const currentChecked =
        this.button.getAttribute("aria-checked") === "true";
      const newChecked = !currentChecked;

      this.button.setAttribute("aria-checked", String(newChecked));

      this.dispatchEvent(
        new CustomEvent("change", {
          detail: { checked: newChecked },
          bubbles: true,
        })
      );
    }

    private handleClick = () => {
      this.toggle();
    };

    private handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === " " || event.key === "Enter") {
        event.preventDefault();
        this.toggle();
      }
    };
  }

  if (!customElements.get("apg-switch")) {
    customElements.define("apg-switch", ApgSwitch);
  }
</script>

Usage

Example
---
import Switch from './Switch.astro';
---

<Switch initialChecked={false}>
  Enable notifications
</Switch>

<script>
  // Listen for change events
  document.querySelector('apg-switch')?.addEventListener('change', (e) => {
    console.log('Checked:', e.detail.checked);
  });
</script>

API

Prop Type Default Description
initialChecked boolean false Initial checked state
disabled boolean false Whether the switch is disabled
class string "" Additional CSS classes

Custom Events

Event Detail Description
change { checked: boolean } Fired when the switch state changes

Slots

Slot Description
default Switch label content

Resources