Skip to content

fix(a11y): hold sidebar small text to WCAG AA contrast - #986

Draft
h4yfans wants to merge 2 commits into
mainfrom
sidebar-small-text-contrast-aa
Draft

fix(a11y): hold sidebar small text to WCAG AA contrast#986
h4yfans wants to merge 2 commits into
mainfrom
sidebar-small-text-contrast-aa

Conversation

@h4yfans

@h4yfans h4yfans commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Sidebar text that carries real information renders at 10–11px, which WCAG AA treats as small text and holds to 4.5:1. Several of those elements were well under the floor on the paper sidebar.

Element Before Token After (paper / white / dark)
SidebarSection collapsed count --sidebar-muted/601.43:1 --sidebar-section-heading 5.07 / 5.16 / 6.42
Tag-group heading --muted-foreground/703.62:1 --sidebar-section-heading 5.07 / 5.06 / 6.01
Tag list show-more control --sidebar-muted1.87:1 --sidebar-section-heading 5.07 / 5.16 / 6.42
Per-tag note count --muted-foreground/401.95:1 --sidebar-section-heading 5.07 / 5.06 / 6.01

The section heading also swapped to hover:text-sidebar-foreground, which is 3.18:1 there — so it lost its guarantee exactly while being pointed at.

Approach

--sidebar-section-heading is the sidebar's de-emphasised small-text token, kept separate from --sidebar-muted because that one also colours chevrons and decorative icon buttons, where darkening is too loud.

Headings drop their hover colour — the chevron fading in, or the row's bg-muted, carries the affordance. The show-more control is a control rather than a label, so it keeps a hover colour, but one that raises the ratio (--sidebar-primary, 15.22:1 on paper) instead of lowering it. Elements whose row paints hover:bg-muted while the text is visible are measured against --muted too, not just --sidebar.

How this is tested

jsdom has no cascade and no layout, so a render can never prove a ratio. The palette is literal hex in base.css, so tests/utils/contrast.ts reads the theme blocks straight from source and does the arithmetic. It checks :root, .white and .dark, understands Tailwind's /NN opacity modifier (text-sidebar-muted/60 is not --sidebar-muted), and fails on any state variant that repaints the text below the floor or merely below where it rested — however the variant is spelled (hover:, group-hover/section:, peer-hover:, [&:hover]:).

Ten new assertions across the two components. Regressing any token or class in this PR turns them red.

One thing worth a reviewer's eye

Both components seed UI state from localStorage, and neither tests/setup-dom.ts nor tests/setup.ts clears it — jsdom is fresh per file but shared by every test inside it. A stored value silently outranks defaultExpanded, collapses the tag category, or reorders tags out from under the maxVisible slice, and which one bit you would depend on test order. Both test files now pin it in beforeEach; the pin was mutation-tested by pre-seeding each key and confirming the target element disappears. Documented in contribute/testing.md since it applies to any renderer component that persists state.

Verification

  • pnpm --filter @memry/desktop test:renderer565 files / 6318 pass, 0 fail
  • The two contrast files: 10/10, green in isolation and in the full suite
  • pnpm typecheck — 16/16 tasks
  • pnpm lint — 0 errors (14 pre-existing warnings, none in these files)
  • pnpm docs:impact --base f4b2fa5cf --strict — covered · pnpm docs:build — clean

Scope

Branched off main and deliberately limited to the a11y work. The Excalidraw context-menu fix and the canvas-folder schema work that shared a worktree with this are not included and remain unpushed.

🤖 Generated with Claude Code

h4yfans added 2 commits August 7, 2026 15:02
Sidebar text that carries real information renders at 10-11px, which WCAG
AA treats as small text and holds to 4.5:1. Several of those elements were
well under the floor on the paper sidebar:

  SidebarSection collapsed count   --sidebar-muted/60      1.43:1
  Tag-group heading                --muted-foreground/70   3.62:1
  Tag list show-more control       --sidebar-muted         1.87:1
  Per-tag note count               --muted-foreground/40   1.95:1

The section heading also swapped to `hover:text-sidebar-foreground`, which
is 3.18:1 there, so it lost its guarantee exactly while being pointed at.

All of them now take `--sidebar-section-heading`, the sidebar's
de-emphasised small-text token, kept separate from `--sidebar-muted`
because that one also colours chevrons and decorative icon buttons where
the extra weight is too loud. Headings drop their hover colour — the
chevron fading in, or the row's `bg-muted`, carries the affordance. The
show-more control is a control rather than a label, so it keeps a hover
colour, but one that raises the ratio instead of lowering it.

Measured against each theme's own surface: 5.07 / 5.16 / 6.42 on
`--sidebar`, and 5.07 / 5.06 / 6.01 on `--muted` for the elements whose
row paints a hover background while the text is visible.

jsdom has no cascade and no layout, so a render can never prove a ratio.
The palette is literal hex in base.css, so tests/utils/contrast.ts reads
the theme blocks straight from source and does the arithmetic. It fails
on any state variant that repaints the text below the floor, or merely
below where it rested, and understands Tailwind's `/NN` opacity modifier
(`text-sidebar-muted/60` is not `--sidebar-muted`).

Both components seed UI state from localStorage and nothing in the
renderer setup clears it, so the tests pin it in `beforeEach`: a stored
value would otherwise outrank `defaultExpanded`, collapse the tag
category, or reorder the tags out from under a `maxVisible` slice, and
which happened would depend on test order.
@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation test labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit f34382a.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant