Most form controls skip the shared Select and Input components #110

Closed
opened 2026-10-09 19:55:35 -04:00 by goddard · 1 comment
Collaborator

Most forms render raw <select> and <input> elements with local styles instead of the shared components, so fields look different from page to page.

  • Select is used in 7 files while 35 render a raw <select>. Input is used in 5 while 42 render a raw <input>. 46 files restyle fields locally.
  • The difference shows: Capacity and Metrics use the shared ▾ (Select.svelte:57,69), while Leaderboard, Matches, Activity and Provisioning show the browser's chevron.
  • ui-patterns.md:10 says to use Field with Input, Select or Textarea, but check-ui-styles.mjs only checks font sizes (:13), so nothing catches raw controls.

Screenshots

Before, Metrics uses the shared Select:
Before

Before, Leaderboard uses a raw select with the browser chevron:
Before

After (mockup), Leaderboard with the shared style:
After (mockup)

Fix

  • Move raw text inputs and selects onto Field with Input or Select
  • Extend check-ui-styles.mjs to flag raw <select> and text <input> outside the primitives
Most forms render raw `<select>` and `<input>` elements with local styles instead of the shared components, so fields look different from page to page. - `Select` is used in 7 files while 35 render a raw `<select>`. `Input` is used in 5 while 42 render a raw `<input>`. 46 files restyle fields locally. - The difference shows: Capacity and Metrics use the shared ▾ (`Select.svelte:57,69`), while Leaderboard, Matches, Activity and Provisioning show the browser's chevron. - `ui-patterns.md:10` says to use `Field` with `Input`, `Select` or `Textarea`, but `check-ui-styles.mjs` only checks font sizes (`:13`), so nothing catches raw controls. **Screenshots** Before, Metrics uses the shared Select: ![Before](https://gitea.thewrightserver.net/attachments/ad9d8da1-107d-4952-a2d9-4509b1c13c95) Before, Leaderboard uses a raw select with the browser chevron: ![Before](https://gitea.thewrightserver.net/attachments/4c883921-ce60-4564-a1dd-3d5b26b83229) After (mockup), Leaderboard with the shared style: ![After (mockup)](https://gitea.thewrightserver.net/attachments/02f98ebb-a22c-4530-a365-f43d8e285d63) **Fix** - [x] Move raw text inputs and selects onto `Field` with `Input` or `Select` - [x] Extend `check-ui-styles.mjs` to flag raw `<select>` and text `<input>` outside the primitives
goddard added the
priority
medium
type/maintainabilitytype/ux
labels 2026-10-09 19:55:35 -04:00
josh was assigned by goddard 2026-10-09 19:55:35 -04:00
Author
Collaborator

Reviewed cc9c10226e. Every select and text-like input now goes through Field with Select or Input, and the style check rejects raw ones, so both fix items are done and I'm closing this.

Checklist

  • Move raw text inputs and selects onto the shared components. Before this commit, 35 files rendered a raw <select>, 42 a raw <input>, 7 used Select and 5 used Input, matching the issue. Now no file outside the primitives renders a raw <select>, and the only raw <input>s left are checkboxes, radios and file pickers. 41 files use Select and 40 use Input. A comparison of every old control with its replacement found the IDs, names, autocomplete values, required, length and pattern limits, ARIA attributes, bindings, handlers and option values unchanged, including on the login and setup forms. In the preview, every visible select on Leaderboard, Matches, Activity, Provisioning, Metrics and Capacity now has the shared ▾ (Select.svelte:55,67) instead of the browser's chevron.
  • Make check-ui-styles.mjs flag raw controls. checkFormControls parses each file's markup with the Svelte compiler and flags every <select>, and every <input> whose type is missing, dynamic or not in the native list at check-ui-styles.mjs:6-17 (check-ui-styles.mjs:23-69). Only primitives/Input.svelte and primitives/Select.svelte are exempt (check-ui-styles.mjs:26). I ran it on a sample with a multi-line <select and <input type={k}>, and it flagged both while passing a checkbox and <Input type="text">. npm run check:styles passes on the current HEAD, and ui-patterns.md:10-22 documents the rule.

Review notes

  • Good extra catches: Input and Select now forward native attributes and merge aria-describedby with the field's message (Input.svelte:46). Field no longer points aria-labelledby at a label that doesn't exist. Error text that used to sit inside some labels is now outside them and linked by aria-describedby. The #104 date-range fix (MatchHistory.svelte:186, ActivityLog.svelte:247,254) and the #115 link and running time still hold after this commit.
  • All 39 ui-* test files pass locally (308 tests). CI run 849 on this commit is green.
  • Minor and non-blocking: two help texts on the new beta page are now loose spans inside Field, so screen readers no longer announce them with their field (betas/new/+page.svelte:678-679,761-762). Before, they were inside the label. Field's hint prop would link them.
  • Minor and non-blocking: some page-scoped rules for text inputs no longer match anything, because the inner <input> now lives in Input and page styles can't reach it. For example, settings/users/+page.svelte:668 styles input:not([type='checkbox']), but that page only has checkboxes left.
  • Minor and non-blocking: a few controls look different now. Activity's "Per page" label sits above its select instead of beside it, because the .pagination label rule was removed. Settings' number fields are 100 px wide instead of 88 px and no longer use tabular figures (SettingsEditor.svelte:532-535).
  • Outside this issue: 6 files still render a raw <textarea>, although ui-patterns.md:10 says to use Textarea: AddHostDrawer, BetaRevisionDialog, BetaTestProgress, ConfigInspector, ConfigTextEditor and ScriptUploadDrawer. The check doesn't look at textareas.
Reviewed cc9c10226e81d0019b516a4f091b0a4df9e3ebd5. Every select and text-like input now goes through `Field` with `Select` or `Input`, and the style check rejects raw ones, so both fix items are done and I'm closing this. **Checklist** - [x] **Move raw text inputs and selects onto the shared components.** Before this commit, 35 files rendered a raw `<select>`, 42 a raw `<input>`, 7 used `Select` and 5 used `Input`, matching the issue. Now no file outside the primitives renders a raw `<select>`, and the only raw `<input>`s left are checkboxes, radios and file pickers. 41 files use `Select` and 40 use `Input`. A comparison of every old control with its replacement found the IDs, names, `autocomplete` values, `required`, length and pattern limits, ARIA attributes, bindings, handlers and option values unchanged, including on the login and setup forms. In the preview, every visible select on Leaderboard, Matches, Activity, Provisioning, Metrics and Capacity now has the shared ▾ (`Select.svelte:55,67`) instead of the browser's chevron. - [x] **Make `check-ui-styles.mjs` flag raw controls.** `checkFormControls` parses each file's markup with the Svelte compiler and flags every `<select>`, and every `<input>` whose type is missing, dynamic or not in the native list at `check-ui-styles.mjs:6-17` (`check-ui-styles.mjs:23-69`). Only `primitives/Input.svelte` and `primitives/Select.svelte` are exempt (`check-ui-styles.mjs:26`). I ran it on a sample with a multi-line `<select` and `<input type={k}>`, and it flagged both while passing a checkbox and `<Input type="text">`. `npm run check:styles` passes on the current HEAD, and `ui-patterns.md:10-22` documents the rule. **Review notes** - Good extra catches: `Input` and `Select` now forward native attributes and merge `aria-describedby` with the field's message (`Input.svelte:46`). `Field` no longer points `aria-labelledby` at a label that doesn't exist. Error text that used to sit inside some labels is now outside them and linked by `aria-describedby`. The #104 date-range fix (`MatchHistory.svelte:186`, `ActivityLog.svelte:247,254`) and the #115 link and running time still hold after this commit. - All 39 `ui-*` test files pass locally (308 tests). CI [run 849](https://gitea.thewrightserver.net/josh/electric-cherry/actions/runs/849) on this commit is green. - Minor and non-blocking: two help texts on the new beta page are now loose spans inside `Field`, so screen readers no longer announce them with their field (`betas/new/+page.svelte:678-679,761-762`). Before, they were inside the label. Field's `hint` prop would link them. - Minor and non-blocking: some page-scoped rules for text inputs no longer match anything, because the inner `<input>` now lives in `Input` and page styles can't reach it. For example, `settings/users/+page.svelte:668` styles `input:not([type='checkbox'])`, but that page only has checkboxes left. - Minor and non-blocking: a few controls look different now. Activity's "Per page" label sits above its select instead of beside it, because the `.pagination label` rule was removed. Settings' number fields are 100 px wide instead of 88 px and no longer use tabular figures (`SettingsEditor.svelte:532-535`). - Outside this issue: 6 files still render a raw `<textarea>`, although `ui-patterns.md:10` says to use `Textarea`: `AddHostDrawer`, `BetaRevisionDialog`, `BetaTestProgress`, `ConfigInspector`, `ConfigTextEditor` and `ScriptUploadDrawer`. The check doesn't look at textareas.
Sign in to join this conversation.