Accessibility baseline: no headings, div buttons, sub-AA contrast #102

Closed
opened 2026-07-28 06:03:10 +00:00 by claude-bot · 1 comment

Severity: MEDIUM

The problem

docs/circa-ui-spec.md §12 commits to semantic labels, contrast, and keyboard operability "from
the start." The current baseline:

  • Zero h1-h6 elements in the entire application. card-title, page-title, and
    login-title are all divs or spans.
  • Div buttons. PhotoBrowserPage.tsx:27-28,64-75 uses div role="button" with Enter-only
    handling and no Space support.
  • Tabs lack ARIA. ReviewWorkspacePage.tsx:510-523 has no role="tab" or aria-selected.
  • State by colour alone. The front/back toggle conveys state through disabled styling and
    colour; the rescan flag uses a bare glyph and colour.
  • Contrast failures. --text-muted #8c857a on #fffdf9 is about 3.6:1; the pending badge
    #8a7a1a on #faf5d0 is about 3.9:1. Both fail AA's 4.5:1, at small sizes.
  • Tiny text. Root font-size: 14px makes .badge and .text-xs 0.75rem = 10.5px.
  • No :focus-visible styling on buttons — browser default only.

Why now

None of this blocks a sighted mouse user today. But every month of new UI built on a heading-free,
div-button foundation raises the retrofit cost, and #45's keyboard work will land on whatever focus
semantics exist at that point. The surface is currently five pages — this is the cheapest it will
ever be.

Scope

  • Real headings, styled to look exactly as they do now.
  • <button> and <Link> for tiles and filter rows. A Link on photo tiles also restores
    middle-click and open-in-new-tab for free.
  • aria-pressed on the front/back toggle; proper tab ARIA.
  • Darken --text-muted to around #6f6a5e and fix the yellow badge pair.
  • Raise badge text to 11-12px.
  • A :focus-visible ring using --accent.
  • Never convey status by colour alone — pair with text or shape.

Done when

  • The page has a correct heading outline
  • All interactive elements are real buttons or links, operable by keyboard
  • Contrast meets AA at the sizes used
  • Focus is always visible
  • No status is conveyed by colour alone

References

  • frontend/src/pages/PhotoBrowserPage.tsx:27-28,64-75
  • frontend/src/pages/ReviewWorkspacePage.tsx:510-523
  • frontend/src/styles.css (tokens at :6-44)
  • docs/circa-ui-spec.md §12
## Severity: MEDIUM ## The problem `docs/circa-ui-spec.md` §12 commits to semantic labels, contrast, and keyboard operability "from the start." The current baseline: - **Zero `h1`-`h6` elements in the entire application.** `card-title`, `page-title`, and `login-title` are all divs or spans. - **Div buttons.** `PhotoBrowserPage.tsx:27-28,64-75` uses `div role="button"` with Enter-only handling and no Space support. - **Tabs lack ARIA.** `ReviewWorkspacePage.tsx:510-523` has no `role="tab"` or `aria-selected`. - **State by colour alone.** The front/back toggle conveys state through disabled styling and colour; the rescan flag uses a bare glyph and colour. - **Contrast failures.** `--text-muted` #8c857a on #fffdf9 is about 3.6:1; the pending badge #8a7a1a on #faf5d0 is about 3.9:1. Both fail AA's 4.5:1, at small sizes. - **Tiny text.** Root `font-size: 14px` makes `.badge` and `.text-xs` 0.75rem = **10.5px**. - **No `:focus-visible` styling** on buttons — browser default only. ## Why now None of this blocks a sighted mouse user today. But every month of new UI built on a heading-free, div-button foundation raises the retrofit cost, and #45's keyboard work will land on whatever focus semantics exist at that point. The surface is currently five pages — this is the cheapest it will ever be. ## Scope - Real headings, styled to look exactly as they do now. - `<button>` and `<Link>` for tiles and filter rows. A `Link` on photo tiles also restores middle-click and open-in-new-tab for free. - `aria-pressed` on the front/back toggle; proper tab ARIA. - Darken `--text-muted` to around #6f6a5e and fix the yellow badge pair. - Raise badge text to 11-12px. - A `:focus-visible` ring using `--accent`. - Never convey status by colour alone — pair with text or shape. ## Done when - [ ] The page has a correct heading outline - [ ] All interactive elements are real buttons or links, operable by keyboard - [ ] Contrast meets AA at the sizes used - [ ] Focus is always visible - [ ] No status is conveyed by colour alone ## References - `frontend/src/pages/PhotoBrowserPage.tsx:27-28,64-75` - `frontend/src/pages/ReviewWorkspacePage.tsx:510-523` - `frontend/src/styles.css` (tokens at :6-44) - `docs/circa-ui-spec.md` §12
claude-bot added this to the v0.3.0 milestone 2026-07-28 06:03:10 +00:00
Author

Done in afdafdd, alongside #97, #103 and #104. CI run #59 green.

Done when:

  • The page has a correct heading outline
  • All interactive elements are real buttons or links, operable by keyboard
  • Contrast meets AA at the sizes used
  • Focus is always visible
  • No status is conveyed by colour alone

The proposed --text-muted value does not work, and this is the finding worth keeping. The issue suggests darkening #8c857a to "around #6f6a5e". Measured against every ground it actually appears on:

ground #6f6a5e #68635a
--bg-panel #fffdf9 5.30 ✓ 5.87 ✓
--bg-card #faf7f2 5.04 ✓ 5.58 ✓
--bg-app #f3f0e8 4.73 ✓ 5.24 ✓
--bg-rail #e6dfd2 4.07 ✗ 4.50 ✓

It clears AA on three grounds and fails on the nav rail, which is the darkest surface in the application and the one the proposal was not checked against. #68635a is the minimum darkening that clears 4.5:1 on all four while holding hue and saturation constant.

The original was worse than the issue states, too: #8c857a on the rail is 2.76:1, below even the 3:1 floor for large text.

--orange (3.61) and --yellow (3.78) were corrected the same way, to #a15d17 and #7c6d17. The badges needed separate values again, because they sit on their own tints rather than on the page: pending 3.90 → 4.51, needs review 3.54 → 4.51, approved 4.35 → 4.52. Disputed already passed at 4.60 and was left alone. Badge text went 10.5px → 13px.

Everything else in scope: real h1/h2 elements where the application had zero headings; the status filter rows became real <button>s (so Space works, which the Enter-only handler never did); photo tiles became <Link>s, which restores middle-click and open-in-new-tab — a reviewer comparing two prints can now put them side by side, which was impossible before; evidence rows added in #96 became real buttons too; role="tablist" / role="tab" / aria-selected / role="tabpanel"; and a single :focus-visible rule covering every focusable element, where only form inputs had one.

Two honest deviations.

There is no h3. The only candidate was the notes revision list, and an h3 there would read in the outline as a child of Tools — the last h2 before the tab strip — because a tab label is not a heading. A shallow-but-correct outline beat a deeper wrong one. If a third level is wanted, the clean way is to give the tabbed region its own h2.

No aria-pressed on the rescan button. Its accessible name already flips between "Flag Rescan" and "Clear Rescan", so a pressed state announces the same fact twice and contradicts it once. The front/back toggle keeps stable names, so aria-pressed is correct there and is applied. The rescan flag also already had visible text — the colour-alone part was real and the glyph is now aria-hidden, with the words carrying the meaning.

Still open, flagged not fixed: superseded evidence is distinguished by 40% opacity alone. Same family as "no status by colour alone", not named in this issue, and fixing it means new copy on the row. Worth its own issue.

Done in afdafdd, alongside #97, #103 and #104. CI run [#59](https://git.rhoving.com/rbrooks/Circa/actions/runs/59) green. **Done when:** - [x] The page has a correct heading outline - [x] All interactive elements are real buttons or links, operable by keyboard - [x] Contrast meets AA at the sizes used - [x] Focus is always visible - [x] No status is conveyed by colour alone **The proposed `--text-muted` value does not work, and this is the finding worth keeping.** The issue suggests darkening `#8c857a` to "around `#6f6a5e`". Measured against every ground it actually appears on: | ground | `#6f6a5e` | `#68635a` | |---|---|---| | `--bg-panel` `#fffdf9` | 5.30 ✓ | 5.87 ✓ | | `--bg-card` `#faf7f2` | 5.04 ✓ | 5.58 ✓ | | `--bg-app` `#f3f0e8` | 4.73 ✓ | 5.24 ✓ | | `--bg-rail` `#e6dfd2` | **4.07 ✗** | 4.50 ✓ | It clears AA on three grounds and fails on the **nav rail**, which is the darkest surface in the application and the one the proposal was not checked against. `#68635a` is the minimum darkening that clears 4.5:1 on all four while holding hue and saturation constant. The original was worse than the issue states, too: `#8c857a` on the rail is **2.76:1**, below even the 3:1 floor for large text. `--orange` (3.61) and `--yellow` (3.78) were corrected the same way, to `#a15d17` and `#7c6d17`. **The badges needed separate values again**, because they sit on their own tints rather than on the page: pending 3.90 → 4.51, needs review 3.54 → 4.51, approved 4.35 → 4.52. Disputed already passed at 4.60 and was left alone. Badge text went 10.5px → 13px. Everything else in scope: real `h1`/`h2` elements where the application had **zero** headings; the status filter rows became real `<button>`s (so Space works, which the Enter-only handler never did); photo tiles became `<Link>`s, which restores middle-click and open-in-new-tab — a reviewer comparing two prints can now put them side by side, which was impossible before; evidence rows added in #96 became real buttons too; `role="tablist"` / `role="tab"` / `aria-selected` / `role="tabpanel"`; and a single `:focus-visible` rule covering every focusable element, where only form inputs had one. **Two honest deviations.** There is **no `h3`**. The only candidate was the notes revision list, and an `h3` there would read in the outline as a child of `Tools` — the last `h2` before the tab strip — because a tab label is not a heading. A shallow-but-correct outline beat a deeper wrong one. If a third level is wanted, the clean way is to give the tabbed region its own `h2`. **No `aria-pressed` on the rescan button.** Its accessible name already flips between "Flag Rescan" and "Clear Rescan", so a pressed state announces the same fact twice and contradicts it once. The front/back toggle keeps stable names, so `aria-pressed` is correct there and is applied. The rescan flag also already had visible text — the colour-alone part was real and the glyph is now `aria-hidden`, with the words carrying the meaning. **Still open, flagged not fixed:** superseded evidence is distinguished by 40% opacity alone. Same family as "no status by colour alone", not named in this issue, and fixing it means new copy on the row. Worth its own issue.
Sign in to join this conversation.
No description provided.