Files
roboco/docs/frontend/components/accessible-icon-buttons.md
T
192524265c [f309463f] Systematic tooltip and aria-label pass across the entire panel (#484)
* [001c9a7a] Author tooltip/aria-label spec for the panel (#469) (#473)

* [001c9a7a] docs(ux_ui): add tooltip/aria-label classification spec for panel controls

* [001c9a7a] docs(ux_ui): commit missing tooltip/aria-label spec content

Prior commit's message claimed to add the spec but only touched
unrelated generated lifecycle prompt files — the actual spec file was
never git-added. This commits the real content.

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>

* [dbe222aa] Implement tooltip and aria-label sweep across all panel surfaces (#478)

* [6f991331] Add aria-label + matching tooltip per tooltip-aria-label-spec.md (#476)

* [6f991331] feat(panel): add aria-label + matching tooltip to 8 icon-only controls per tooltip-aria-label-spec.md §1a/§1b, wrap assignee-avatar initials in a full-name tooltip

* [6f991331] docs(accessibility): add icon-only controls pattern guide for aria-label + matching tooltip

Documented the implemented pattern for accessible icon-only controls across 8 components (bell, back-arrow, menu, toggle, drag-handle, move-forward, settings, review-link) plus the assignee-avatar tooltip. Covers when to apply the pattern, naming conventions, state-dependent labels, testing approach, and rationale for local TooltipProvider scope.

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [e34da833] Fix notification-bell.tsx and assignee-avatar.tsx, re-verify all 9 claimed tooltip/aria-label retrofits (#480)

* [e34da833] test(notifications): add regression coverage confirming the bell button's aria-label/title/Tooltip and re-verify the other 8 tooltip-aria-label-spec controls by direct file read

* [e34da833] docs(ux_ui): update tooltip-aria-label-spec.md status to "implemented" with test coverage summary

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [09414273] fix(header): wrap refresh button in Tooltip; correct spec.md and accessible-icon-buttons.md doc-accuracy issues (#483)

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>

---------

Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>

* [f309463f] fix: missing tooltip/Link/ArrowLeft imports + dedupe command-center tooltip import, drop redundant native title on refresh button, reflow doc prose

- kanban-card.tsx, header.tsx: import TooltipProvider (used but undefined -> eslint react/jsx-no-undef, blocked Panel lint + QA image panel build)
- task-header.tsx: import Link (next/link) and ArrowLeft (lucide-react) for the back button tooltip
- command-center.tsx: remove the duplicate tooltip primitive import block (kept the one with TooltipProvider; tsc duplicate-identifier)
- header.tsx: drop native title= on the refresh button now that a Radix Tooltip carries the hint (header test expects no native title)
- docs/frontend/components/accessible-icon-buttons.md: reflow hard-wrapped prose (python gate make reflow-docs)

* [f309463f] chore: regenerate lifecycle artifacts + verb tables (reconcile after master merge)

The branch's generated intro prose in agents/prompts/_generated/lifecycle-*.md
and verbs.md had drifted to unwrapped lines (master is wrapped). The foundation-
check gate (make lifecycle + regenerate_verb_tables + git diff --exit-code) caught
the drift. Re-rendered via the canonical generators; no hand-edits.

* [f309463f] Close remaining a11y gaps: aria-labels on task-table row-expand + pagination, titles on work-session truncated task-id/branch, secretary Start loading label

---------

Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech>
Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-13 06:38:48 +02:00

4.7 KiB

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:

import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

const LABEL = "Open settings";

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <Button
        aria-label={LABEL}
        title={LABEL}
      >
        <Settings className="h-5 w-5" />
      </Button>
    </TooltipTrigger>
    <TooltipContent>{LABEL}</TooltipContent>
  </Tooltip>
</TooltipProvider>

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:

const toggleLabel = sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar";

<Button
  aria-label={toggleLabel}
  title={toggleLabel}
>
  {/* ... */}
</Button>

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:

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <Avatar>
        <AvatarFallback>{initials}</AvatarFallback>
      </Avatar>
    </TooltipTrigger>
    <TooltipContent>{fullName}</TooltipContent>
  </Tooltip>
</TooltipProvider>

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:

  1. 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 <TooltipProvider> (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