A11y Async Button

Core example

Icon and text states

A semantic async button changes both its leading icon and visible label as a content sync action moves through loading, success, error, and reset states.

Quick facts

What this example covers

Import path
a11y-async-button
Pattern
Core async button with custom icon rendering
Render hook
renderText(button, text, state)
States shown
Idle, loading, success, error, and reset

Live demo

Sync button icon and label

A publishing team needs one action that clearly says what is happening. The icon is decorative, but it changes alongside the text so sighted users get the same state cue without relying on color.

Production copy sync

idle

Ready to sync production copy.

User goal

Confirm that the same button communicates progress and outcome through text, icon shape, state color, busy semantics, repeat-click prevention, and a polite status announcement.

  • The icon span is marked aria-hidden="true".
  • The text span remains the accessible button name.
  • The status element is reused as the plugin live region.

Try it

Useful checks

Example code

Use the renderText option

The plugin still controls locking, busy state, state classes, live announcements, and reset behavior. The demo only customizes how the current state text is rendered inside the button.

import { createAsyncButton } from "a11y-async-button";
import "a11y-async-button/styles.css";

const button = document.querySelector("[data-a11y-async-button]");

const icons = {
  idle: cloudUploadIcon,
  loading: syncingIcon,
  success: cloudCheckIcon,
  error: warningIcon
};

if (button instanceof HTMLButtonElement) {
  createAsyncButton(button, {
    renderText(button, text, state) {
      button.querySelector("[data-a11y-async-button-text]").textContent = text;
      button.querySelector("[data-async-button-icon]").innerHTML =
        icons[state] ?? icons.idle;
    },
    onAction: async () => {
      await syncProductionCopy();
    }
  });
}

Developer notes

Copyable pieces

Selectors and initialization

  • Root selector: [data-a11y-async-button] on a real <button>.
  • Text selector: [data-a11y-async-button-text] remains the accessible name source.
  • Icon selector: [data-async-button-icon] is demo markup controlled by renderText.
  • CSS import: a11y-async-button/styles.css for default layout, state colors, focus, and motion handling.

Options used

  • renderText updates the label and decorative icon together.
  • onAction lets the plugin manage loading, success, and error automatically.
  • data-preserve-width prevents the button from jumping between shorter and longer labels.
  • data-live-region points announcements at the visible status element.

Accessibility notes

What changes for users

Keyboard, focus, and status

  • The control is a native button, so Enter and Space activate it without custom key handling.
  • Focus stays on the button while the async action runs.
  • The plugin sets busy and disabled semantics while repeat activation is blocked.
  • The visible status element receives the same state message through a polite live region.

Visual state and limitations

  • Each state changes icon shape and text, so state is not conveyed by color alone.
  • The loading icon may rotate, and the default CSS disables that motion for reduced-motion users.
  • The icon is decorative. Do not remove or hide the text span unless you provide an equivalent accessible name.
  • Keep SVG icons inline or local so the button remains framework-free and CDN-free.