Accessibility — Kata docs
Kata docs

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);
});