# Accessible Icon-Only Controls (aria-label + Tooltip) ## Overview Icon-only controls—buttons without visible text—require two layers of accessibility to be usable by all: 1. **aria-label** attribute for screen reader users 2. **Visible Tooltip** (matching text) for mouse, keyboard, and screen reader users Both layers must use identical text that describes the action or result. ## Pattern All icon-only controls in the panel follow this structure: ```typescript import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; const LABEL = "Open settings"; {LABEL} ``` ### Why three attributes? - **aria-label**: The accessible name for screen readers - **title**: Browser native tooltip (fallback, appears on hover/focus) - **TooltipContent**: Radix UI tooltip for consistent visual feedback ### Naming convention Label text uses **active verbs** describing what happens when the control is clicked: | Control | Label | |---------|-------| | Settings gear | "Open settings" | | Notification bell | "View notifications" | | Back arrow | "Go back to tasks list" | | Collapse toggle | "Collapse sidebar" / "Expand sidebar" | | Drag handle | "Drag to move task between columns" | | Menu trigger | "Open task actions menu" | Avoid passive voice ("Settings opened") or generic labels ("Button"). ## State-dependent labels When a control's action varies by state, compute the label dynamically: ```typescript const toggleLabel = sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"; ``` The label updates whenever state changes, keeping screen reader users informed. ## Truncated content (avatars, badges) When an icon-only control displays shortened content (e.g., "FD1" for "Frontend Dev 1"), wrap in a tooltip showing the full value: ```typescript {initials} {fullName} ``` ## When NOT to use this pattern **Do not** apply aria-label + tooltip to: - **Text-labeled buttons** — the text is the label - **Decorative icons** — non-interactive graphics (use `aria-hidden="true"` instead) - **Self-labeling badges** — content + styling conveys meaning - **Chart tooltips** — handled by the charting library (recharts, etc.) ## Implemented controls The following 8 icon-only controls have been retrofitted: 1. **notification-bell.tsx** — "View notifications" 2. **task-header.tsx** (back button) — "Go back to tasks list" 3. **task-actions.tsx** (menu) — "Open task actions menu" 4. **sidebar.tsx** (collapse toggle) — "Collapse sidebar" / "Expand sidebar" 5. **kanban-card.tsx** (drag handle) — "Drag to move task between columns" 6. **kanban-card.tsx** (move-forward) — "Move forward" / "PM must activate this task first" 7. **command-center.tsx** (settings) — "Open settings" 8. **pr-review-queue.tsx** (details link) — "Review details" Plus one avatar tooltip: 9. **assignee-avatar.tsx** — Shows full agent display name ## Testing ### Screen reader 1. Tab to the icon-only control 2. Verify the aria-label is announced 3. Focus should be visible and clear ### Mouse 1. Hover over the control 2. Tooltip appears with the same text as aria-label 3. Click triggers the expected action ### Keyboard 1. Tab to focus the control 2. title attribute provides a browser tooltip 3. Verify the label is consistent ### State changes 1. For conditional labels, verify the label updates when state changes 2. Tab away and back to re-announce the new label ## TooltipProvider scope Each component wraps its controls in a local `` (not a single app-root provider). This pattern: - Keeps tooltip state scoped to the component - Matches existing codebase patterns - Simplifies DOM structure and reduces global state If a future refactoring uses a root-level provider, the structure remains valid—only the wrapping changes, not the aria-label/title pattern. ## Resources - [WCAG 2.1: Text Alternatives for Images](https://www.w3.org/WAI/WCAG21/Understanding/text-alternatives) - [ARIA Authoring Practices: Buttons](https://www.w3.org/WAI/ARIA/apg/patterns/button/) - [Radix UI Tooltip](https://www.radix-ui.com/docs/primitives/components/tooltip) - Local test files: See `kanban-card-aria.test.tsx`, `sidebar.test.tsx`, `assignee-avatar.test.tsx`