---
name: keyboard-accessibility-spec
description: Analyzes a selected frame for keyboard accessibility — generates numbered tab-order badges, optional F6 landmark section-jump pills (only when multiple sections exist), and a documentation panel on the canvas with per-element keyboard interaction details and ARIA landmark mapping.
disable-model-invocation: true
---
<!-- type: custom-skill -->
# Keyboard Accessibility Spec

Analyze the selected frame for keyboard accessibility. Produce canvas artifacts appropriate to the component's complexity:

1. **Tab-order badge overlay** — a sibling frame layered over the selected frame with numbered blue circular badges on every interactive element
2. **F6 landmark pills** _(only if 2+ landmark sections are identified)_ — teal pill-shaped badges marking each major page section for F6 section-jumping
3. **Documentation panel** — a white panel to the right of the frame with the keyboard interaction spec and per-element key behaviors. When 2+ sections exist, includes a Section Navigation Map and F6 implementation guidance. When only 1 section exists (typical for individual components), these are omitted for a cleaner, more focused spec.

---

## Running this with Claude and the Figma MCP

Written for Figma's built-in agent, adapted for Claude (Claude Code, Claude Desktop or claude.ai) connected to the official Figma MCP server.

- **Get the file.** If you don't have a Figma link, ask for one. The file key is the segment after `/design/`; a `node-id=1-2` parameter means node `1:2`.
- **Start a session.** Call `create_session` with the file key before any other Figma tool, and pass the returned session ID on every later call.
- **Read before writing.** `get_metadata` shows the structure and the user's current selection, `get_variable_defs` lists the variables on a node, `search_design_system` finds published components, variables and styles, and `get_screenshot` shows what's on the canvas.
- **Write with `use_figma`.** It runs Plugin API JavaScript in the file. Load the `figma-use` skill first (plus `figma-generate-library` when you create variables, styles or components) and list them in `skillNames`. Scripts use top-level `await` and `return` everything they create: nothing persists between calls, so pass node and variable IDs forward as string literals. Colors are 0–1, not 0–255. Switch pages with `await figma.setCurrentPageAsync(page)`, at most once per call; for multi-page work, run one call per page in parallel. `figma.notify()` isn't available.
- **Ask, then wait.** Wherever a step says to ask the user, ask in chat (in Claude Code, use the AskUserQuestion tool) and wait for the answer before building.
- **Clear helper fills.** `figma.createAutoLayout()` frames start with a white fill. Set `fills = []` on rows and wrappers that should be transparent, or they show up as white bars (most visibly in dark-mode previews).
- **Check the result.** After a build step, take one screenshot (`await node.screenshot()` inside `use_figma`, or `get_screenshot`) and fix only what's wrong.

## Step 1: Identify interactive elements

Use a read-only `use_figma` script to recursively traverse the selected frame's layer tree (get the selected frame's ID from `get_metadata`). Identify interactive elements by name patterns and node structure:

| Pattern (case-insensitive name match) | Classification |
|---|---|
| "button", "btn", "cta" | `button` |
| "icon button", small (24–48px) frame with vector/icon child | `icon-button` |
| "input", "field", "text field", "search", "textarea" | `text-input` |
| "dropdown", "select", "picker", "combo" | `dropdown` |
| "checkbox", "check box", "toggle" | `checkbox` |
| "radio" | `radio-group` |
| "link", "breadcrumb", or text with underline | `link` |
| "date", "calendar" | `date-picker` |
| Children of "nav", "sidebar", "menu", "tab bar" | `sidebar-item` |
| Frames containing page numbers or prev/next arrows | `pagination` |

Skip decorative elements (dividers, icons inside buttons, labels, background shapes).

## Step 2: Determine tab order

Sort all interactive elements into a sequential tab order:

1. **Visual reading order** — top-to-bottom, then left-to-right within each horizontal band
2. **Section grouping** — elements within the same visual section stay together
3. **Radio groups** — count as a single tab stop. Sub-label individual radios with letter suffixes (e.g. 51a, 51b, 51c). Arrow keys move between radios within the group
4. **Data tables** — tab through cells left-to-right per row, then to the next row. Recommend `role="grid"` with arrow-key cell navigation for tables with many cells

## Step 3: Identify landmark sections

Group tab stops into major page regions. For each section, assign:

- A **letter** (A, B, C, ...) representing F6 jump order
- An **ARIA landmark role** from this table:

| Section type | ARIA role | HTML element |
|---|---|---|
| Page header / top bar | `banner` | `<header>` |
| Navigation sidebar or nav bar | `navigation` | `<nav aria-label="...">` |
| Primary content area | `main` | `<main>` |
| Side panel / preview pane | `complementary` | `<aside aria-label="...">` |
| Named content section | `region` | `<section aria-label="...">` |
| Action bar / icon toolbar | `toolbar` | `<div role="toolbar" aria-label="...">` |
| Page footer / bottom actions | `contentinfo` | `<footer>` |

- The **first focusable element** within the section (where F6 lands focus)

**After this step, count the total number of landmark sections. Store this as `sectionCount`. This value controls conditional output in Steps 4 and 5.**

## Step 4: Place badge overlay

Create a canvas-level sibling frame positioned exactly over the selected frame. This overlay must NOT be a child of the target frame — it must be appended to `figma.currentPage` to avoid disrupting auto-layout.

### Overlay frame setup

    const overlay = figma.createFrame();
    overlay.name = "Keyboard Tab Order Overlay";
    overlay.resize(frame.width, frame.height);
    overlay.x = frame.absoluteTransform[0][2];
    overlay.y = frame.absoluteTransform[1][2];
    overlay.fills = [];
    overlay.clipsContent = false;
    overlay.layoutMode = "NONE";
    // Append to figma.currentPage — NOT the target frame

Use `absoluteTransform` to position badges. For each element, compute its position relative to the overlay's origin:

    const relX = element.absoluteTransform[0][2] - frame.absoluteTransform[0][2];
    const relY = element.absoluteTransform[1][2] - frame.absoluteTransform[1][2];

### Tab-stop badges (blue circles)

For each tab stop, create a 28×28 blue circle (#2563EB) with white centered text showing the tab-stop number. Position it at the top-left corner of the element. If two badges would overlap (elements within 30px vertically), offset one to avoid occlusion — e.g. place one on the left edge and the other on the right edge of their respective elements.

### Landmark badges (teal pills) — only if `sectionCount >= 2`

**Skip this subsection entirely if only 1 section was identified.** F6 section-jumping is a multi-region navigation strategy — it adds no value when the entire selected frame is a single region.

For each F6 section, create a teal (#0D8E85) rounded pill labeled "F6 [letter] [section name]" (e.g. "F6 A Header"). Use white text, corner radius 10, horizontal padding 10, vertical padding 4. Position it at the anchor element for that section.

Load fonts before creating any text nodes:

    await figma.loadFontAsync({ family: "Inter", style: "Bold" });
    await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" });
    await figma.loadFontAsync({ family: "Inter", style: "Regular" });
    await figma.loadFontAsync({ family: "Inter", style: "Medium" });

## Step 5: Build documentation panel

Create a white frame with rounded corners (12px) to the right of the target frame (100px gap). Use vertical auto-layout, 48px padding, 24px item spacing.

### Critical: Text sizing in auto-layout

Always append nodes to their parent FIRST, then set sizing. This prevents truncation and overlap.

For text nodes:

    parent.appendChild(textNode);
    textNode.layoutAlign = "STRETCH";
    textNode.layoutSizingHorizontal = "FILL";
    textNode.textAutoResize = "HEIGHT";

For section frames:

    parent.appendChild(sectionFrame);
    sectionFrame.layoutAlign = "STRETCH";
    sectionFrame.layoutSizingHorizontal = "FILL";
    sectionFrame.layoutSizingVertical = "HUG";

Never set `layoutSizingHorizontal`, `layoutSizingVertical`, or `textAutoResize` before appending the node to its auto-layout parent. The node must be a child of an auto-layout frame before these properties are set, or they will be ignored or throw errors.

### Multi-column layout for tab-stop details — only if `sectionCount >= 2`

**If `sectionCount` is 1, use a single flat block instead of the multi-column layout.** The per-section grouping and column packing add unnecessary complexity for a single-region component.

When `sectionCount >= 2`, the per-section tab-stop details must be arranged in a **multi-column horizontal layout** to minimize vertical scrolling. Use this algorithm:

1. Build each F6 section's content as an independent block (a vertical auto-layout frame with a light gray background, 8px corner radius, 16px padding, 8px item spacing)
2. Set a **maximum column height of 2000px**
3. Pack section blocks into columns top-to-bottom. When adding the next block would push a column past 2000px, start a new column
4. If a single section block exceeds 2000px on its own, give it its own column
5. Wrap all columns in a horizontal auto-layout frame with 32px spacing, set to `layoutSizingHorizontal = "FILL"` and each column to `layoutSizingHorizontal = "FILL"`

Set the overall doc width dynamically: `(number of columns) × 450 + padding`. For example, 4 columns → ~1950px wide.

### Color system

| Role | Color | Use |
|---|---|---|
| Blue | `{r:0.145, g:0.388, b:0.922}` | Tab-stop numbers and element names |
| Teal | `{r:0.05, g:0.55, b:0.52}` | Section headers and ARIA landmark info |
| Gray | `{r:0.35, g:0.35, b:0.35}` | Key behaviors and descriptions |
| Amber | `{r:0.55, g:0.4, b:0.05}` | Implementation notes and warnings |
| Black | `{r:0.13, g:0.13, b:0.13}` | Section titles |

### Typography

Use Inter throughout:
- **Bold 28px** — document title
- **Bold 20px** — full-width section headings (Overview, Keyboard Reference, Implementation Notes)
- **Bold 16px** — per-section column headings (F6 A — Header) _(multi-section only)_
- **Semi Bold 14px** — landmark cards, implementation note card titles
- **Semi Bold 12px** — tab-stop entry labels (number + element name + type), sub-section labels within blocks (e.g. "Row 1", "Radio Group")
- **Regular 12–13px** — body text, key behaviors
- **Medium 11–12px** — ARIA role annotations, implementation notes

### Documentation sections

**1. Title block** (full width)
Screen name as Bold 28px, subtitle as Medium 16px gray.

**2. Overview** (full width)

- **If `sectionCount >= 2`:** Total tab stops, total landmark sections, and a plain-language explanation of the dual navigation strategy:
  - F6/Shift+F6 section jumping — allows all keyboard users to cycle between major page regions, reducing the "N-tab problem"
  - ARIA landmarks — allows screen reader users to jump between sections using their assistive technology's built-in landmark navigation

- **If `sectionCount` is 1:** Total tab stops only. Describe the keyboard navigation model for this component (Tab order, arrow keys, Enter/Space). Do NOT mention F6 section jumping or ARIA landmarks — these are page-level concerns irrelevant to an individual component.

**3. Section Navigation Map** — only if `sectionCount >= 2`

**Skip this section entirely if only 1 section was identified.**

(card grid, wrapping horizontal layout)
For each landmark section, create a small card (light teal-tinted background, 8px corner radius) showing:
- F6 letter and section name (teal semi-bold)
- ARIA role and recommended HTML element
- Tab-stop range and where F6 lands focus
Use `layoutWrap = "WRAP"` on the parent frame so cards flow into rows automatically. Each card should be a fixed width (~430px).

**4. Keyboard Reference** (card grid, wrapping horizontal layout)
A set of cards (light gray background, 8px corner radius) for each interaction type found in the component. Each card shows the type name (semi-bold) and its key behaviors (gray). Use `layoutWrap = "WRAP"` so cards flow into rows.

- **If `sectionCount >= 2`:** Include a leading "Section Navigation" card covering F6/Shift+F6 and screen reader landmark nav.
- **If `sectionCount` is 1:** Omit the "Section Navigation" card. Only show cards for the interaction types actually present in the component.

Interaction types to include (only those present in the component):
- Text Input: Tab: Focus | Arrows: Cursor | Esc: Revert
- Dropdown: Tab: Focus | Enter/Space: Open | Arrows: Options | Esc: Close
- Date Picker: Tab: Focus | Enter/Space: Calendar | Arrows: Days | Esc: Close
- Checkbox: Tab: Focus | Enter/Space: Toggle
- Radio Group: Tab: Focus selected | Arrows: Move | Enter/Space: Select
- Button: Tab: Focus | Enter/Space: Activate
- Link: Tab: Focus | Enter: Navigate
- Sidebar Item: Tab: Focus | Enter/Space: Navigate | Arrows: Move
- Pagination: Tab: Focus | Enter/Space: Select page | Arrows: Pages

**5. Tab Stop Details**

- **If `sectionCount >= 2`:** Use the heading "Tab Stop Details by Section" in Bold 20px, followed by the multi-column frame containing all F6 section blocks (see "Multi-column layout" above). Each section block contains:
  - Section heading: "F6 [letter] — [Section Name] ([start]–[end])" in Bold 16px
  - ARIA annotation: role, HTML element, and F6 landing element in teal Medium 11px
  - Contextual notes where applicable

- **If `sectionCount` is 1:** Use the heading "Tab Stop Details" in Bold 20px, followed by a single flat block (light gray background, 8px corner radius, 16px padding, 8px item spacing). Do NOT include F6 letter labels, ARIA landmark annotations, or section sub-headers — list tab stops directly.

Each tab stop as an entry:
  - Number, element name, and type (blue semi-bold 12px)
  - Key behaviors: Tab, Enter/Space, Arrows, Escape (gray 11px, indented 4px)
  - Implementation notes where relevant (amber medium 11px, indented 4px) — required fields, aria attributes, special behaviors

Special interaction patterns to document within entries:
- **Radio groups**: Label as single tab stop, sub-label individual radios with letter suffixes (51a, 51b, 51c), note "Not a separate tab stop" on non-selected radios
- **Data tables**: Group by row with "Row N" sub-headers, note grid entry/exit behavior, recommend role="grid"
- **Toolbars**: Note "Tab enters/exits toolbar, arrow keys navigate between tools"
- **Pagination**: Note aria-disabled on boundary buttons, aria-current on active page

**6. Implementation Notes** (card grid, wrapping horizontal layout)

- **If `sectionCount >= 2`:** Four cards covering:
  - **F6 Section Jumping**: Global keydown listener, ordered section array, wrap-around, aria-live announcements
  - **ARIA Landmark Mapping**: Unique roles + aria-labels, testing with NVDA/JAWS/VoiceOver, avoiding duplicate unlabeled roles
  - **Focus Management**: Skip-to-main link, error focus, modal focus trapping, focus restoration, aria-live status messages
  - **Data Tables**: role="grid", arrow key cell navigation, aria-rowindex/aria-colindex

- **If `sectionCount` is 1:** Only include implementation note cards relevant to the component's interaction patterns. Omit the "F6 Section Jumping" and "ARIA Landmark Mapping" cards. Focus on:
  - **Component-specific ARIA pattern** (e.g. WAI-ARIA Accordion Pattern, Listbox, Combobox)
  - **Focus Management** relevant to the component (expand/collapse, modal traps, focus restoration)
  - **State Management** (collapsed/expanded, disabled states, aria-hidden)

Use `layoutWrap = "WRAP"` on the parent frame. Each card ~400–680px wide (scale to content), light gray background, 8px corner radius, 16px padding.

Separate all major top-level sections with 1px gray (#E0E0E0) dividers.

## Step 6: Verify output

After generating all artifacts, use `await node.screenshot()` on both the overlay and the documentation panel to verify:
- Badge positions align with interactive elements
- No badge occlusion (overlapping badges)
- No text truncation or overlap in the documentation
- All sections render with proper hierarchy and spacing
- Color coding is consistent
- F6 content is absent when only 1 section exists
- No column in the tab-stop details exceeds 2000px in height (multi-section only)
