Accessibility
Blueprint builds keyboard support and ARIA attributes into its components, and Clawscale keeps them. This page covers what your app still owns: focus, contrast, labels and motion.
Focus rings#
The focused element gets a 2px ring in --cs-color-focus. ClawscaleProvider shows it for keyboard focus only, so mouse clicks leave no ring. Pass focusRings="always" to show it for every focus, as Blueprint does by default. FocusStyleManager switches the same behavior at runtime.
Turn off the switch, click a button, then press Tab.
import { Button, FocusStyleManager, InputGroup, Switch } from "@clawscale/react";
import { useEffect, useState } from "react";
export default function AccessibilityFocus() {
const [keyboardOnly, setKeyboardOnly] = useState(false);
useEffect(() => setKeyboardOnly(FocusStyleManager.isActive()), []);
const toggle = (checked: boolean) => {
if (checked) FocusStyleManager.onlyShowFocusOnTabs();
else FocusStyleManager.alwaysShowFocus();
setKeyboardOnly(checked);
};
return (
<div style={{ display: "grid", gap: 12, width: 320 }}>
<Switch
checked={keyboardOnly}
label="Show focus only for keyboard"
onChange={(event) => toggle(event.currentTarget.checked)}
/>
<InputGroup aria-label="Pipeline name" placeholder="Pipeline name" />
<div style={{ display: "flex", gap: 8 }}>
<Button intent="primary" text="Run pipeline" />
<Button text="Cancel" />
</div>
</div>
);
}--cs-color-focus keeps at least 3:1 contrast against every surface in every theme and color scheme. Use it for focus styles in your own components.
.run-row:focus-visible {
outline: 2px solid var(--cs-color-focus);
outline-offset: 2px;
}Color contrast#
Unit tests check the color tokens of every theme against WCAG AA in both color schemes. They live in packages/tokens/test/tokens.test.ts and run in CI.
| Pair | Minimum ratio |
|---|---|
text, text-muted and text-subtle on every surface | 4.5:1 |
text on hover, active and selected rows | 4.5:1 |
text on controls in every state | 4.5:1 |
Intent -on text on its solid fill, at rest, hover and active | 4.5:1 |
Intent -text on surface and on its -subtle fill | 4.5:1 |
neutral-on on neutral fills | 4.5:1 |
tooltip-text on tooltip | 4.5:1 |
focus on every surface | 3:1 |
text-disabled is exempt: WCAG does not require contrast for disabled controls. If you override a color token, check its pairs again.
Never show status by color alone. Pair an intent with an icon or a label.
Icon-only buttons#
A button with only an icon has no text for screen readers. Give it an aria-label. Wrap it in a Tooltip with the same text for mouse users. The tooltip also opens on keyboard focus.
import { Button, Tooltip } from "@clawscale/react";
export default function AccessibilityIconButtons() {
return (
<>
<Tooltip content="Refresh">
<Button icon="refresh" variant="minimal" aria-label="Refresh" />
</Tooltip>
<Tooltip content="Filter rows">
<Button icon="filter" variant="minimal" aria-label="Filter rows" />
</Tooltip>
<Tooltip content="Download CSV">
<Button icon="download" variant="minimal" aria-label="Download CSV" />
</Tooltip>
<Tooltip content="Delete pipeline">
<Button icon="trash" intent="danger" variant="minimal" aria-label="Delete pipeline" />
</Tooltip>
</>
);
}Icons and inputs#
Blueprint hides icons from screen readers by default. Pass title to an Icon that carries meaning on its own. Label every input with FormGroup and labelFor, or with aria-label.
Reduced motion#
Clawscale stops the skeleton pulse when the system asks for reduced motion. Blueprint's other animations ignore that setting. Read prefers-reduced-motion and stop motion you do not need, such as progress bar stripes.
import { Classes, ProgressBar } from "@clawscale/react";
import { useSyncExternalStore } from "react";
const QUERY = "(prefers-reduced-motion: reduce)";
function subscribe(onChange: () => void) {
const media = window.matchMedia(QUERY);
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}
function usePrefersReducedMotion() {
return useSyncExternalStore(
subscribe,
() => window.matchMedia(QUERY).matches,
() => false,
);
}
export default function AccessibilityReducedMotion() {
const reduced = usePrefersReducedMotion();
return (
<div style={{ display: "grid", gap: 8, width: 320 }}>
<ProgressBar aria-label="Backfill progress" animate={!reduced} intent="primary" value={0.64} />
<span className={Classes.TEXT_MUTED}>
Backfill 64% done. {reduced ? "Reduced motion is on: stripes stay still." : "Stripes move."}
</span>
</div>
);
}