kanban
v0.1.12File-backed kanban board — tickets, threaded comments, agent assignment, the kanban:change and kanban:comment trigger types, and an injectable console board with agent profiles for planning and review.
Install
iii trigger compose::add worker=kanban@0.1.12Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.
skill doc
web-accessibility
how-toWeb accessibility
Target WCAG 2.2 level AA. Anything less is a bug filed against your users, and it is cheapest to build in.
Standard: https://www.w3.org/WAI/WCAG22/quickref/ · patterns: https://www.w3.org/WAI/ARIA/apg/patterns/ (APG). Read the APG pattern before hand-rolling any composite widget — dialog, tabs, menu, combobox, listbox, tree, grid.
Rule zero: use the element that already does it
A native element arrives with role, keyboard behavior, focus, and screen-reader announcements. ARIA only describes; it never implements.
| Need | Use | Not |
|---|---|---|
| do something | |
|
| go somewhere | |
|
| labeled input | + |
placeholder-as-label |
| group of choices | |
a styled |
| navigation | , , , , |
nested |
| headings | – in order |
|
The first ARIA rule: if a native element or attribute can do it, do not use ARIA. A wrong
role is worse than no role — it overwrites correct implicit semantics.
Keyboard: the whole interface, no mouse
Test every change by unplugging the mouse mentally and walking the flow with Tab / Shift+Tab / Enter / Space / Escape / arrows.
- Everything interactive is reachable and operable by keyboard, in a logical order that
follows the visual order.
tabindexvalues greater than 0 are almost always wrong. - Focus is always visible. Never
outline: nonewithout an equally visible replacement. Style:focus-visibleso mouse users do not see rings and keyboard users always do. - Escape closes the topmost overlay; focus returns to the element that opened it.
- Modals trap focus and make the rest of the page inert —
withshowModal(), or theinertattribute. Closing must restore focus deliberately; the browser will not. - Composite widgets follow the APG's key contract: roving tabindex or
aria-activedescendantfor tabs and menus, arrow keys to move, Home/End to jump, typeahead in listboxes. - A skip-to-content link as the first focusable element on full-page layouts.
- Do not hijack scroll or single-key shortcuts without an off switch (WCAG 2.1.4).
- Route changes need focus management. On client-side navigation move focus to the new
(or a container withtabIndex={-1}) and updatedocument.title, or a screen-reader user hears nothing change.
Forms
- Every control has a programmatic label. Placeholder text is a hint, never a label.
- Group related controls with
/; a group of radios needs both. - Errors:
aria-invalidon the field, an error message associated witharia-describedby, and an error summary at the top of a long form linking to each offending field. Validate on blur or submit, not on every keystroke. - Required fields:
requiredplus a visible marker, not color alone. - Help the input method:
type="email",inputMode="numeric",autoComplete="one-time-code", and generous touch targets. - Never disable the submit button as the only feedback — show what is missing.
- Autofill and paste must work; do not block them.
Images, color, motion
altis required on every. Informative: describe the content's purpose. Text in an image: put the text inalt. Decorative:alt=""(an empty string, not a missing attribute). A complex chart needs a text alternative, not analtparagraph.- SVG:
role="img"+when meaningful,aria-hidden="true"when decorative. An icon-only button needs an accessible name:aria-labelon the button, not on the. - Contrast: 4.5:1 for body text, 3:1 for large text and for UI-component boundaries and focus indicators. Placeholder text and disabled text are the usual failures.
- Never encode meaning in color alone (status, validation, chart series) — pair it with text, an icon, or a pattern.
- Respect
prefers-reduced-motion: reduce: disable parallax, autoplay, and large movement. - WCAG 2.2 additions: focus must not be entirely hidden by sticky headers (2.4.11), and dragging interactions need a single-pointer alternative (2.5.7).
Live regions and async UI
- Announce results that appear without a focus change:
role="status"(aria-live="polite") for counts, saves, and toasts;role="alert"(assertive) only for errors that need interruption. - The live region container must exist in the DOM before you write into it. Inserting the container and the text in the same commit announces nothing.
- Loading states need an accessible name (
aria-busy, a visually-hidden "Loading posts"), not just a spinner. - Do not steal focus to announce.
Verifying — automated tools catch about a third
- Automated:
eslint-plugin-jsx-a11yin CI, plus axe (@axe-core/reactin dev,vitest-axe/jest-axein tests, or the browser extension). Fix everything it reports, then remember how much it cannot see. - Keyboard pass on the real page: complete the primary flow with no mouse. Note the focus ring at every step.
- Zoom and reflow: 200% browser zoom, and a 320 px-wide viewport, with no loss of content or function and no horizontal scrolling (1.4.10 / 1.4.4).
- Screen reader smoke test: VoiceOver (⌘F5) or NVDA. Check the page title, the landmark list, that headings outline the page, that each control announces a name/role/state, and that dynamic updates are announced.
- Testing Library asserts by role and name —
getByRole('button', { name: 'Save' })fails when the accessible name is missing, which is exactly the regression you want caught.
The short list to refuse
divwithonClickinstead of a button.outline: nonewith no focus style.- Placeholder as a label.
aria-labelon a non-interactive element to "give it a name".- A modal that does not trap focus, or that drops focus on close.
- A route change that moves nothing and announces nothing.
titleattributes as the only explanation.- Autoplay media, or animation that ignores reduced-motion.
- Accessibility "checked later": retrofitting semantics costs multiples of building it in.