Accessibility
Kata’s a11y story has two halves: the engine emits rich accessibility hints on every frame, and the React bindings ship hooks that wire those hints into real DOM behavior.
Engine-side hints
Every KSONFrame carries an optional a11y field:
engine.on("update", (frame) => {
console.log(frame.a11y);
// Text: { role: "dialog", liveRegion: "assertive", label: "Narrator says: Welcome" }
// Choice: { role: "group", keyHints: [{ choiceId: "c_0", hint: "Press 1 for Fight" }] }
// Tween: { description: "stranger animates x", reducedMotion: true }
});
Hints are generated by generateA11yHints(action) — a pure function that takes an action and returns aria metadata. You can call it yourself if you’re rendering outside React.
React hooks
useReducedMotion()
Tracks the prefers-reduced-motion media query. Use it to skip tween animations (or switch to instant transitions) for users who opt out:
import { useReducedMotion } from "@kata-framework/react";
function StrangerSprite({ x }) {
const reduced = useReducedMotion();
return <img style={{ transform: `translateX(${x}px)`, transition: reduced ? "none" : "transform 0.8s" }} />;
}
useKeyboardNavigation(choices, onSelect)
Arrow keys, Enter, and number keys (1–9) for choice selection — all with focus management. Drop it in a choice component:
import { useKeyboardNavigation } from "@kata-framework/react";
function ChoiceList({ choices, onPick }) {
const { focusedIndex, handlers } = useKeyboardNavigation(choices, onPick);
return (
<ul {...handlers} role="group" aria-label="Choices">
{choices.map((c, i) => (
<li key={c.id} aria-selected={i === focusedIndex} tabIndex={i === focusedIndex ? 0 : -1}>
{c.label}
</li>
))}
</ul>
);
}
useFocusManagement(dep)
Auto-focuses a ref when a dependency changes — usually when a new frame arrives. Pair it with liveRegion: "assertive" on text frames so screen readers announce the new dialogue:
import { useFocusManagement } from "@kata-framework/react";
function DialogueLine({ frame }) {
const ref = useFocusManagement(frame.state.currentActionIndex);
return (
<div ref={ref} role="dialog" aria-live="assertive" tabIndex={-1}>
{frame.action.content}
</div>
);
}
The KataDebug overlay
The optional KataDebug component ships with ARIA attributes baked in — role, aria-live, aria-label — so your debug overlay doesn’t interfere with screen readers.
Testing a11y
StoryTestRunner exposes dialogueLog and speakerLog, so you can assert that every text frame has a speaker (and not just “unknown”) and that critical information isn’t hidden behind a tween that reduced-motion users won’t see:
test("every text frame has a named speaker", () => {
const story = new StoryTestRunner([intro]);
story.start("intro");
story.advanceUntilChoice();
expect(story.speakerLog.every((s) => s && s !== "unknown")).toBe(true);
});