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
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
- Activate the button and watch the icon and text both change during loading.
- Leave Simulated result on Success and confirm the success icon, label, badge, and status text update.
- Switch Simulated result to Error, activate the button again, and confirm the retry-worthy error state is not color-only.
- Use Tab, Enter, and Space to confirm native keyboard activation and focus retention.
- Use Reset to return the icon, label, badge, and visible status to idle.
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 byrenderText. - CSS import:
a11y-async-button/styles.cssfor default layout, state colors, focus, and motion handling.
Options used
renderTextupdates the label and decorative icon together.onActionlets the plugin manage loading, success, and error automatically.data-preserve-widthprevents the button from jumping between shorter and longer labels.data-live-regionpoints 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.