Skip to content

fix(a11y): label form controls, add a main landmark, fix light-mode contrast - #10

Merged
ClaraVnk merged 2 commits into
mainfrom
fix/a11y-seo
Jul 20, 2026
Merged

fix(a11y): label form controls, add a main landmark, fix light-mode contrast#10
ClaraVnk merged 2 commits into
mainfrom
fix/a11y-seo

Conversation

@ClaraVnk

Copy link
Copy Markdown
Contributor

Contexte

Lighthouse accessibility was 88 on the landing page and 96 on the paste page. SEO is already 100 since the sitemap work in #3, so the 92 in the original report is gone — nothing needed there.

before after
landing page a11y 88 100
paste page a11y 96 100
SEO 100 100

Changements

Form controls had no accessible name. The visible text lives in <div class="option-label">, while <label class="toggle"> wraps only the switch graphic — so screen readers announced eight controls as unlabelled checkboxes and selects. They now reference that visible text with aria-labelledby rather than duplicating it into aria-label: one source of truth, and assistive tech reads the same words sighted users see.

The password, confirm-password and webhook fields relied on placeholders, which are not accessible names and disappear on input. They get explicit aria-labels. Lighthouse never flagged these three because they sit behind toggles and it only audits the default state.

Added a <main> landmark. body is a flex column and .page-bg/.page-plain claim flex: 1 as its children, so main forwards both the growth and the axis. Layout is unchanged — verified against before/after screenshots of both page types.

Light-mode contrast. The paste page action chips used raw Dracula hues, which are built for dark backgrounds:

element before after
Copy content 1.04:1 5.5:1
Raw 1.18:1 5.7:1
Download 1.23:1 5.6:1
paste id 2.53:1 5.6:1

1.04:1 is not "low contrast", it is invisible text. The paste id compounded the problem by stacking opacity: 0.7 on an already-muted colour. All four now resolve through semantic tokens that darken under [data-theme="light"] and fall through to the raw palette in dark mode — dark theme computes byte-identical, verified in the browser.

Two more contrast bugs Lighthouse structurally cannot see, because both notices render conditionally (owner-only, just-created): .notice-info and .owner-notice.just-created sat at ~1.3:1 in light mode. Found by reading the CSS after noticing the "save this link" line looked washed out in a screenshot.

var(--muted) was never defined. An undefined var() is invalid at computed-value time, so the declaration is dropped and the element inherits its parent's colour. That is why the "save this link" sub-text rendered in Dracula cyan — in both themes, not just light. It now uses var(--text-muted).

Tests

116 pass, including a new guard asserting no stylesheet declaration references an undefined custom property. Confirmed it catches the original bug:

guard would flag: ['--muted']

Browser-verified: the owner notice was rendered by actually creating a paste — the state Lighthouse cannot reach — and its computed colours checked in both themes. Dark mode returns rgb(80, 250, 123) for both the notice and the copy button, identical to before.

Risques

The <main> wrapper is the only structural change. It needed a CSS rule to forward the flex behaviour; without it the page would stop filling the viewport. Screenshots of both page types confirm no shift, but it is worth a look on a narrow viewport if you have one handy.

The light-mode colour values are hand-picked to clear AA with margin (all ≥5.4:1) while keeping each action's hue identity. They are darker than a designer might choose — if any look muddy to you, they can move lighter as long as they stay above 4.5:1.

ClaraVnk added 2 commits July 20, 2026 12:59
…ontrast

Lighthouse accessibility was 88 on the landing page and 96 on the paste page.
SEO is already 100 since the sitemap work, so nothing was needed there.

Form controls had no accessible name. The visible text lives in
<div class="option-label">, while <label class="toggle"> wraps only the switch
graphic, so screen readers announced eight controls as unlabelled checkboxes
and selects. They now point at that visible text with aria-labelledby: one
source of truth, and assistive tech reads the same words sighted users do. The
password, confirm and webhook fields relied on placeholders, which are not
accessible names and vanish on input; they get explicit aria-labels. Lighthouse
never reached those three because they sit behind toggles.

Added a <main> landmark. body is a flex column and .page-bg/.page-plain claim
flex: 1 as its children, so main forwards both the growth and the axis. Layout
is unchanged, verified against before/after screenshots of both page types.

Light-mode contrast. The action chips on the paste page used raw Dracula hues,
which are designed for dark backgrounds: copy at 1.04:1, raw 1.18:1, download
1.23:1 — invisible rather than merely low-contrast. The paste id stacked
opacity 0.7 on an already-muted colour for 2.53:1. All four now resolve through
semantic tokens that darken in light mode and fall through to the raw palette
in dark mode, which is left byte-identical.

Two more contrast bugs Lighthouse structurally cannot see, because both notices
render conditionally: .notice-info and .owner-notice.just-created sat at ~1.3:1
in light mode. Fixed the same way.

And the cause of one of them: var(--muted) was never defined. An undefined var()
is invalid at computed-value time, so the declaration is dropped and the element
inherits its parent's colour — which is why the "save this link" sub-text
rendered in Dracula cyan, in both themes. It now uses var(--text-muted), and a
test asserts no stylesheet declaration references an undefined custom property.

  landing page  a11y 88 -> 100   seo 100
  paste page    a11y 96 -> 100
Both branches appended tests to test_api.py; kept both sets.
@ClaraVnk
ClaraVnk merged commit 4f81202 into main Jul 20, 2026
3 checks passed
@ClaraVnk
ClaraVnk deleted the fix/a11y-seo branch July 20, 2026 11:06
@ClaraVnk ClaraVnk mentioned this pull request Jul 20, 2026
ClaraVnk added a commit that referenced this pull request Jul 20, 2026
Nothing has shipped since v1.5.0: production still serves that build, so the
SEO, privacy, performance and accessibility work merged in #3 and #7-#10 is
present on main and absent from ghostbit.dev.

Pushes to main only publish the `edge` and `sha-*` image tags. The semver and
`latest` tags — which is what a pinned deployment follows — are produced only
by a v*.*.* git tag, and the last one predates all of it.

Server changes in this release:
  - meta description, canonical link, /sitemap.xml, noindex on paste pages
  - star count fetched server-side; no third-party call from the browser and
    connect-src back to 'self'
  - landing page 928 KB -> 336 KB, LCP 6.0s -> 3.0s; paste page 515 KB -> 307 KB
  - form controls labelled, <main> landmark, light-mode contrast fixed
  - dead static assets removed, including a source map misnamed .sha256

Note the CLI has no changes since v1.5.0, yet this publishes ghostbit-cli 1.6.0
to PyPI with identical code: release.yml reads its version from
cli/pyproject.toml, so a server-only release cannot be cut without it. Worth
decoupling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant