{
  "componentId": "checkbox",
  "componentName": "Checkbox",
  "lastReviewed": "2026-05-05",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus moves to the input. Focus ring renders on the input visual square (or on the wrapping label per design system convention). The label is not separately focusable."
    },
    {
      "keys": "Space (input focused)",
      "expected": "Toggles the checked state. Unchecked → checked, checked → unchecked, mixed → checked (canonical APG tri-state path; some implementations use mixed → unchecked instead — pick one and document). `aria-checked` flips and the indicator glyph re-renders."
    },
    {
      "keys": "Enter (input focused)",
      "expected": "Does NOT toggle the checkbox in canonical implementations (Enter is reserved for form submission per HTML spec). Some libraries bind Enter to toggle for symmetry with other custom controls; canonical guidance is to leave Enter to the form-default behaviour."
    },
    {
      "keys": "Tab (group of checkboxes)",
      "expected": "Each checkbox is its own tab stop (independent of sibling state). Unlike Radio (which uses roving tabindex within a group), Checkbox group siblings all participate in the tab order so users can pick any subset."
    }
  ],
  "announcements": [
    {
      "trigger": "SR encounters an unchecked checkbox",
      "expected": "\"Checkbox, [label text], not checked\". The role, accessible name, and state are all announced. For required fields, \"required\" is included before the state."
    },
    {
      "trigger": "SR encounters a checked checkbox",
      "expected": "\"Checkbox, [label text], checked\"."
    },
    {
      "trigger": "SR encounters a mixed (indeterminate) checkbox",
      "expected": "\"Checkbox, [label text], partially checked\" (or \"mixed\" depending on the SR). Activating moves to checked or unchecked per the implementation's tri-state path."
    },
    {
      "trigger": "SR encounters an invalid checkbox after submit",
      "expected": "\"Checkbox, [label text], not checked, invalid entry, [error message]\". The error-message slot's content announces via `aria-describedby` and `aria-invalid`."
    }
  ],
  "axeRules": [
    "aria-allowed-attr",
    "aria-required-attr",
    "aria-valid-attr-value",
    "label",
    "color-contrast"
  ],
  "_about": "Per-component a11y-acceptance data shaped for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe.\n\nSuggested wiring:\n- axeRules → pass to AxeBuilder.options({ runOnly: { type: \"rule\", values: axeRules } }) so the run targets only the rules the canon has pinned for this component (other rules can run in your global pass).\n- keyboardWalk → iterate the entries; each `keys` is a human-readable sequence (e.g. \"Tab → Tab → Esc\"). Translate to page.keyboard.press calls and assert `expected` against the result (focused element, aria-state, visible text, etc.).\n- announcements → assert text content of any aria-live region or capture the accessibility tree at the trigger moment and match against `expected`.\n\nEmpty sub-arrays mean the canon does not yet pin behaviour for that axis on this component, not that none is required."
}