Three things a page says with colour alone, and one it barely says: the field border, the current navigation link, a finished calendar event, and focus on a field #274

Closed
opened 2026-09-18 20:34:53 +00:00 by tiagoagueda · 0 comments
Owner

From an accessibility read of the stylesheet and templates on 2026-09-18. axe runs over every page in both themes and passes; none of this is text contrast, which is what axe measures. Ratios below are computed from the ink tokens in assets/css/app.css:53-75.

Field boundaries are about 1.5:1 in both themes

assets/css/app.css:255-260 (field-input: border-ink-300 bg-white, dark:border-ink-700 dark:bg-ink-950), applied to every input, select and textarea by the base rule, to btn-secondary (:268-271) and to the table filters in partials/table/head.html:80-122.

border vs field border vs page field vs page
light 1.5:1 1.5:1 1.04:1
dark 1.9:1 1.7:1 1.09:1

The border is the only thing that says this is a field, and SC 1.4.11 (non-text contrast, AA) wants 3:1 for that boundary. Nothing handles prefers-contrast: more either. Hurts low-vision users on every form.

Fix: a per-theme field-border token that clears 3:1 (light around L 0.65; ink-500's dark value already does) on field-input and btn-secondary, darker again under prefers-contrast: more. A unit test that reads the tokens and computes the ratio would hold it, the way the theme tests read the palette.

Focus on a field is the weakest cue in the application

app.css:258 sets focus:outline-hidden, which discards the base 2px brand-500 outline every button, link and summary keeps (:211-213), and leaves a one-pixel border change plus a ring at 20% alpha, about 1.2:1 against white. The forced-colours rule (:811-819, #227) restores an outline only in high-contrast mode. SC 2.4.7 passes by a hair; the controls people type into get the faintest indicator. Fix: an opaque ring (focus-visible:ring-2 focus-visible:ring-brand-500), or keep the base outline with offset 0 on fields.

partials/nav_links.html:9-10 renders the active item as a class alone (nav_active_names, core/templatetags/postulo.py:435-442); the style is bg-ink-100 on a white header, 1.1:1, or bg-ink-800 on ink-900, 1.2:1. No aria-current. The sidebar (partials/sidebar.html:16) and the calendar's view switcher do it right. SC 1.4.1 and 4.1.2: colour-blind and low-vision users cannot see which section they are in, screen readers are not told, and forced colours drop the highlight entirely. Fix: emit aria-current="page" in nav_links.html, and give the active link a non-colour cue, weight or a 2px bottom border, which forced colours repaint. A test in tests/test_navigation.py that the active item carries aria-current.

A done reminder or a cancelled interview is conveyed by a strike-through and fading

applications/calendar.html:14-19 (#204): a muted event gets line-through opacity-60; the hidden prefix says only "Interview:" or "Reminder:", and in compact mode event.detail is dropped. At 60% opacity over white the 12px text lands at about 3.1:1 (SC 1.4.3), and nothing tells a screen reader the event is over (SC 1.3.1). axe only sees the contrast if the fixture calendar holds a muted event. Fix: drop opacity-60, keep the strike, add an sr-only word for the state.

Small, while there

The required-field asterisk in partials/field.html:11 (text-red-600) has no dark variant, about 3.8:1 on the ink-900 card, and nothing on the page says what it means; dark:text-red-400 and a "(required)" in the label or a once-per-form line.

From an accessibility read of the stylesheet and templates on 2026-09-18. axe runs over every page in both themes and passes; none of this is text contrast, which is what axe measures. Ratios below are computed from the `ink` tokens in `assets/css/app.css:53-75`. ## Field boundaries are about 1.5:1 in both themes `assets/css/app.css:255-260` (`field-input`: `border-ink-300 bg-white`, `dark:border-ink-700 dark:bg-ink-950`), applied to every input, select and textarea by the base rule, to `btn-secondary` (`:268-271`) and to the table filters in `partials/table/head.html:80-122`. | | border vs field | border vs page | field vs page | | --- | --- | --- | --- | | light | 1.5:1 | 1.5:1 | 1.04:1 | | dark | 1.9:1 | 1.7:1 | 1.09:1 | The border is the only thing that says *this is a field*, and SC 1.4.11 (non-text contrast, AA) wants 3:1 for that boundary. Nothing handles `prefers-contrast: more` either. Hurts low-vision users on every form. **Fix:** a per-theme field-border token that clears 3:1 (light around L 0.65; `ink-500`'s dark value already does) on `field-input` and `btn-secondary`, darker again under `prefers-contrast: more`. A unit test that reads the tokens and computes the ratio would hold it, the way the theme tests read the palette. ## Focus on a field is the weakest cue in the application `app.css:258` sets `focus:outline-hidden`, which discards the base 2px brand-500 outline every button, link and summary keeps (`:211-213`), and leaves a one-pixel border change plus a ring at 20% alpha, about 1.2:1 against white. The forced-colours rule (`:811-819`, #227) restores an outline only in high-contrast mode. SC 2.4.7 passes by a hair; the controls people type into get the faintest indicator. **Fix:** an opaque ring (`focus-visible:ring-2 focus-visible:ring-brand-500`), or keep the base outline with offset 0 on fields. ## The current navigation link is marked by colour only, and not marked at all for a screen reader `partials/nav_links.html:9-10` renders the active item as a class alone (`nav_active_names`, `core/templatetags/postulo.py:435-442`); the style is `bg-ink-100` on a white header, 1.1:1, or `bg-ink-800` on `ink-900`, 1.2:1. No `aria-current`. The sidebar (`partials/sidebar.html:16`) and the calendar's view switcher do it right. SC 1.4.1 and 4.1.2: colour-blind and low-vision users cannot see which section they are in, screen readers are not told, and forced colours drop the highlight entirely. **Fix:** emit `aria-current="page"` in `nav_links.html`, and give the active link a non-colour cue, weight or a 2px bottom border, which forced colours repaint. A test in `tests/test_navigation.py` that the active item carries `aria-current`. ## A done reminder or a cancelled interview is conveyed by a strike-through and fading `applications/calendar.html:14-19` (#204): a muted event gets `line-through opacity-60`; the hidden prefix says only "Interview:" or "Reminder:", and in compact mode `event.detail` is dropped. At 60% opacity over white the 12px text lands at about 3.1:1 (SC 1.4.3), and nothing tells a screen reader the event is over (SC 1.3.1). axe only sees the contrast if the fixture calendar holds a muted event. **Fix:** drop `opacity-60`, keep the strike, add an `sr-only` word for the state. ## Small, while there The required-field asterisk in `partials/field.html:11` (`text-red-600`) has no dark variant, about 3.8:1 on the `ink-900` card, and nothing on the page says what it means; `dark:text-red-400` and a "(required)" in the label or a once-per-form line.
tiagoagueda added this to the 0.4.0 milestone 2026-09-18 20:34:53 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#274
No description provided.