Selects in the dark theme: the choice list is themed for form fields only, and no test looks at a select in either theme #655

Closed
opened 2026-10-03 01:58:37 +00:00 by tiagoagueda · 0 comments
Owner

From the maintainer's list: “Most of the select fields do not respect dark theme.” No page, browser or system is named. Read from the stylesheet, the closed control is painted dark in every select; what is left to the browser, for the fourteen selects that sit outside a form field, is the open list. Nothing tests either. This issue asks for the reproduction first, then one rule and a browser test that holds it.

What exists today

  • Dark is a variant, not a media query. assets/css/app.css:111-126 defines dark as an explicit data-theme="dark" on the root, else prefers-color-scheme. src/postulo/templates/base.html:2 stamps the profile's choice; src/postulo/static/js/app.js:701-708 stamps it as the header switch is pressed.
  • The browser is told which theme is in force. assets/css/app.css:383-399 (compiled src/postulo/static/css/app.css:4745-4755) sets color-scheme on the root in the same three states. #9 asked for this; it is done. The operating system draws the open list from it.
  • The closed control is one element rule for every select: assets/css/app.css:432-445, bg-white … dark:bg-ink-950 dark:text-ink-100, in @layer base.
  • The open list is painted only by Basecoat's native-select.css: option, optgroup { bg-[Canvas] text-[CanvasText] }, scoped to .field > select and select.select (compiled :3022-3039). Every other select leaves it to inherit the select's text colour over whatever the browser draws. assets/css/basecoat.css:325-343 sets appearance: auto on .field > select, select.select, .input-group > select and, since #305, .input-group > select[data-service-select]; a country or a language in a group is drawn without the native chevron, as the comment there says.
  • A select in an input group is made transparent with color: var(--color-foreground) (compiled :3104-3130, in the components layer, which outranks the base rule); the group paints dark:bg-ink-950 (assets/css/basecoat.css:394-398).
  • No test sees it. Axe runs over every page in both schemes (tests/e2e/test_accessibility.py:661, :699), but a select's popup is the platform's and is in neither the page nor a screenshot, and no test reads a select's computed colours, an option's, or color-scheme.

The selects, by family (paths under src/postulo/templates/):

Family Closed box List Where
Directly in .field dark themed every field drawn by <c-field>; and applications/partials/board_filters.html:22,31,40, applications/partials/bulk_fields.html:7,14,20, applications/report.html:55,81, jobs/merge.html:171, jobs/partials/bulk_fields.html:8,14, jobs/partials/listing_bulk_fields.html:14,22, server/logs.html:44,53, server/design.html:157,196
Bare, outside any .field dark not themed applications/partials/application_card.html:64 (the board's status), applications/suggestion_list.html:82, core/import_csv_map.html:35,58, cotton/table/filter.html:76 (every table's column filter), documents/letter_detail.html:74, jobs/posting_detail.html:37, partials/table/columns.html:40 (page size)
In an .input-group dark, by the group not themed partials/phone_widget.html:38-41 (country of a number), partials/postal_addresses.html:91-93 (country), partials/web_links.html:48-50 (the service), server/defaults.html:32-34 (default language), server/design.html:180,209

What cannot be established from here is which of these looks wrong on the maintainer's screen, and in which browser. The open list is drawn by the platform; the page can state only color-scheme and the options' own colours.

Checked against main at f09b6010b.

Proposal

  1. Reproduce before changing anything. On this issue: browser and version, operating system, whether the dark theme came from the profile or the system, the page, and a screenshot of the open list. The families above say where to look.
  2. One rule for every list. Move the option paint out of Basecoat's two selectors into assets/css/app.css, onto select option, select optgroup, in the same tokens as the closed box (bg-white text-ink-900 dark:bg-ink-950 dark:text-ink-100), so the list is the colour of the box it opens from. Forced colours keep the system pair, as the rest of the file does.
  3. Say it in the input group. The group's select states its own text colour for its list, since it is transparent over the group.
  4. Rebuild with npm run build:css and commit the compiled stylesheet.
  5. The browser test below, run first: it should fail on the option assertions for the second and third families, and if it does not, step 1 is what is left to explain.

Decisions

  • Tokens or system colours for the list. Canvas is the browser's near-black in dark and white in light; the closed box is ink-950 and white. Recommended: tokens, so the box and its list are one colour. System colours stay for forced colours.
  • Wait for #301? #301 replaces the native select with Basecoat's everywhere, and keeps the native one as the scripts-off path (the maintainer's comment of 2026-09-29). Recommended: do not wait. The native select stays on every page with scripts off and wherever the script has not run, so the bug stays real after #301.
  • Where the closed select is already fine. Left alone; the change is to the list.

Needed by

Issue #301 (axe clean in both themes) can take its test from here.

Done when

  • tests/e2e/test_dark_selects.py (new, -m e2e): for one select of each family above, in three states (profile dark, nothing stamped with the system dark, profile light with the system dark), it reads getComputedStyle for color-scheme, the select, and each of its options, and asserts that the root's color-scheme is dark or light as stated, that the select's text and ground clear 4.5:1, and that every option has an opaque ground and text of its own that clear 4.5:1 and are dark in the dark states and light in the light one.
  • tests/test_contrast.py gains the same check on the tokens the list now uses, in both themes.
  • Axe stays clean in both schemes (tests/e2e/test_accessibility.py), and tests/test_stylesheet.py passes on the rebuilt stylesheet.
  • No strings, no record, no archive change.

From the list of improvements of 2026-10-02.

From the maintainer's list: “Most of the select fields do not respect dark theme.” No page, browser or system is named. Read from the stylesheet, the closed control is painted dark in every select; what is left to the browser, for the fourteen selects that sit outside a form field, is the open list. Nothing tests either. This issue asks for the reproduction first, then one rule and a browser test that holds it. ## What exists today - **Dark is a variant, not a media query.** `assets/css/app.css:111-126` defines `dark` as an explicit `data-theme="dark"` on the root, else `prefers-color-scheme`. `src/postulo/templates/base.html:2` stamps the profile's choice; `src/postulo/static/js/app.js:701-708` stamps it as the header switch is pressed. - **The browser is told which theme is in force.** `assets/css/app.css:383-399` (compiled `src/postulo/static/css/app.css:4745-4755`) sets `color-scheme` on the root in the same three states. #9 asked for this; it is done. The operating system draws the open list from it. - **The closed control** is one element rule for every select: `assets/css/app.css:432-445`, `bg-white … dark:bg-ink-950 dark:text-ink-100`, in `@layer base`. - **The open list** is painted only by Basecoat's `native-select.css`: `option, optgroup { bg-[Canvas] text-[CanvasText] }`, scoped to `.field > select` and `select.select` (compiled `:3022-3039`). Every other select leaves it to inherit the select's text colour over whatever the browser draws. `assets/css/basecoat.css:325-343` sets `appearance: auto` on `.field > select`, `select.select`, `.input-group > select` and, since #305, `.input-group > select[data-service-select]`; a country or a language in a group is drawn without the native chevron, as the comment there says. - **A select in an input group** is made transparent with `color: var(--color-foreground)` (compiled `:3104-3130`, in the components layer, which outranks the base rule); the group paints `dark:bg-ink-950` (`assets/css/basecoat.css:394-398`). - **No test sees it.** Axe runs over every page in both schemes (`tests/e2e/test_accessibility.py:661`, `:699`), but a select's popup is the platform's and is in neither the page nor a screenshot, and no test reads a select's computed colours, an option's, or `color-scheme`. The selects, by family (paths under `src/postulo/templates/`): | Family | Closed box | List | Where | | --- | --- | --- | --- | | Directly in `.field` | dark | themed | every field drawn by `<c-field>`; and `applications/partials/board_filters.html:22,31,40`, `applications/partials/bulk_fields.html:7,14,20`, `applications/report.html:55,81`, `jobs/merge.html:171`, `jobs/partials/bulk_fields.html:8,14`, `jobs/partials/listing_bulk_fields.html:14,22`, `server/logs.html:44,53`, `server/design.html:157,196` | | Bare, outside any `.field` | dark | **not themed** | `applications/partials/application_card.html:64` (the board's status), `applications/suggestion_list.html:82`, `core/import_csv_map.html:35,58`, `cotton/table/filter.html:76` (every table's column filter), `documents/letter_detail.html:74`, `jobs/posting_detail.html:37`, `partials/table/columns.html:40` (page size) | | In an `.input-group` | dark, by the group | **not themed** | `partials/phone_widget.html:38-41` (country of a number), `partials/postal_addresses.html:91-93` (country), `partials/web_links.html:48-50` (the service), `server/defaults.html:32-34` (default language), `server/design.html:180,209` | What cannot be established from here is which of these looks wrong on the maintainer's screen, and in which browser. The open list is drawn by the platform; the page can state only `color-scheme` and the options' own colours. Checked against `main` at `f09b6010b`. ## Proposal 1. **Reproduce before changing anything.** On this issue: browser and version, operating system, whether the dark theme came from the profile or the system, the page, and a screenshot of the open list. The families above say where to look. 2. **One rule for every list.** Move the option paint out of Basecoat's two selectors into `assets/css/app.css`, onto `select option, select optgroup`, in the same tokens as the closed box (`bg-white text-ink-900 dark:bg-ink-950 dark:text-ink-100`), so the list is the colour of the box it opens from. Forced colours keep the system pair, as the rest of the file does. 3. **Say it in the input group.** The group's select states its own text colour for its list, since it is transparent over the group. 4. **Rebuild** with `npm run build:css` and commit the compiled stylesheet. 5. **The browser test below,** run first: it should fail on the option assertions for the second and third families, and if it does not, step 1 is what is left to explain. ## Decisions - **Tokens or system colours for the list.** `Canvas` is the browser's near-black in dark and white in light; the closed box is ink-950 and white. **Recommended:** tokens, so the box and its list are one colour. System colours stay for forced colours. - **Wait for #301?** #301 replaces the native select with Basecoat's everywhere, and keeps the native one as the scripts-off path (the maintainer's comment of 2026-09-29). **Recommended:** do not wait. The native select stays on every page with scripts off and wherever the script has not run, so the bug stays real after #301. - **Where the closed select is already fine.** Left alone; the change is to the list. ## Needed by Issue #301 (axe clean in both themes) can take its test from here. ## Done when - `tests/e2e/test_dark_selects.py` (new, `-m e2e`): for one select of each family above, in three states (profile dark, nothing stamped with the system dark, profile light with the system dark), it reads `getComputedStyle` for `color-scheme`, the select, and each of its options, and asserts that the root's `color-scheme` is `dark` or `light` as stated, that the select's text and ground clear 4.5:1, and that every option has an opaque ground and text of its own that clear 4.5:1 and are dark in the dark states and light in the light one. - `tests/test_contrast.py` gains the same check on the tokens the list now uses, in both themes. - Axe stays clean in both schemes (`tests/e2e/test_accessibility.py`), and `tests/test_stylesheet.py` passes on the rebuilt stylesheet. - No strings, no record, no archive change. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.5.0 milestone 2026-10-03 01:58:37 +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#655
No description provided.