Skip to content

feat: pure black dark mode with blue story links - #31

Merged
ericboehs merged 2 commits into
masterfrom
feat/dark-mode-link-color
Aug 30, 2026
Merged

feat: pure black dark mode with blue story links#31
ericboehs merged 2 commits into
masterfrom
feat/dark-mode-link-color

Conversation

@ericboehs

Copy link
Copy Markdown
Owner

Story titles rendered white in dark mode. Two separate UIkit rules caused it:

.uk-light a                { color: #fff }              ← titles go white
.uk-light .uk-text-primary { color: #fff !important }   ← high-score highlight erased

The second one is the quieter bug: a 328-point story looked identical to a 55-point one, because the blue uk-text-primary highlight was overwritten.

Colour choice

Picked by measurement rather than eye. Against the new pure-black background:

Colour Contrast on #000
UIkit #1e87f0 5.78 AA only — and fails AA (4.38) on the old #222
#58a6ff 8.31 AAA

Pure black for OLED

Background moves from rgb(34,34,34) to #000 so OLED pixels switch off. That exposed the #DDD thread borders at 15:1 against the page — louder than the text beside them — so they drop to #2a2a2a, with hover reusing the link blue.

A no-JS bug this fixes

Dark mode previously took the background from the media query but the text colour from .uk-light, which only JS applies. With JS off that rendered #666 on #2221.9:1, unreadable. The palette is now gated behind .hn-dark, so it falls back to the light theme instead.

The script also moves to <head> so the background is set before first paint rather than flashing white, and listens for media-query changes instead of re-checking twice a second for the life of the tab.

Verification

Driven through Chrome over the DevTools protocol, emulating each scheme and reading computed styles:

dark light dark, JS disabled
background rgb(0,0,0) rgb(255,255,255) rgb(255,255,255)
story link rgb(88,166,255) rgb(13,13,13)
score rgb(88,166,255) rgb(30,135,240)
thread border rgb(42,42,42)
<html> class hn-dark none none

Review-driven fixes

The detail page was still white. The title link there sits in an <h3>, so neither selector reached it — and that's the page the request was about. Measured at rgb(255,255,255), now rgb(88,166,255).

Deploys couldn't retire cached CSS. ETags came only from database rows, so a template change kept returning 304 to clients holding an old validator. A busy listing recovers on the next worker write; a settled thread never changes and could serve the old stylesheet indefinitely. Templates now feed a TEMPLATE_VERSION digest — verified that touching a view changes every ETag.

A comment of mine was wrong about a:visited needing to follow another rule for source order; it's more specific and wins regardless.

Tests: 119 runs, 320 assertions.

Dark mode rendered every link white, because UIkit's .uk-light sets
`a { color: #fff }`. That erased the distinction between a story title
and ordinary text. It also sets `.uk-text-primary { color: #fff
!important }`, which silently removed the blue high-score highlight, so
a 328-point story looked identical to a 55-point one.

Story links and the score highlight are now #58a6ff. Measured against
the new background it is 8.3:1, clearing WCAG AAA; UIkit's own #1e87f0
only reaches 5.8:1 and fails AA outright on the previous #222.

Background is pure black so OLED pixels switch off rather than draw dark
grey. That made the #DDD thread borders glaring -- 15:1 against the page,
louder than the text beside them -- so they drop to #2a2a2a, with the
hover border reusing the link blue.

The dark palette is now gated behind .hn-dark, which the script adds.
Previously the dark background came from the media query alone while the
text colour came from .uk-light via JS, so with JS disabled the page
rendered #666 text on a #222 background: a contrast ratio of 1.9:1.
It now falls back to the light palette, which is at least readable.

The script moves to <head> and runs synchronously so the background is
set before first paint instead of flashing white, and it listens for
media-query changes rather than re-checking twice a second for the life
of the tab.

Verified with Chrome over the DevTools protocol, emulating each colour
scheme and reading computed styles: dark gives rgb(0,0,0) background
with rgb(88,166,255) links and score, borders rgb(42,42,42); with script
execution disabled the html element carries no class and the background
stays white.
The story detail page title stayed white. Its link sits in an <h3>, so
neither the .story nor the table selector reached it -- and that page is
exactly the one the change was asked for. Confirmed in the browser at
rgb(255,255,255); the heading now carries .story and reads rgb(88,166,255)
like the listing.

Validators were built only from database rows, so a template change alone
kept serving 304 to anyone holding an older ETag. A busy listing recovers
on the next worker write, but a settled thread never changes, so it could
serve the old stylesheet indefinitely. A digest of the templates now feeds
the key: touching a view changes TEMPLATE_VERSION and every ETag with it.

Corrected a wrong comment: a:visited is a class-level component more
specific than the rule above it, so it wins on specificity, not source
order. Also softened the claim that white links are indistinguishable
from body text -- uk-light body text is translucent white, so the
difference is small rather than absent.

Loosened the dark-mode test, which asserted exact CSS and JS source and
would have failed on reformatting while passing on broken rendering. It
now checks only that the dark background cannot apply without the
scripted class, and says so.
@ericboehs
ericboehs merged commit b15a139 into master Aug 30, 2026
2 checks passed
@ericboehs
ericboehs deleted the feat/dark-mode-link-color branch August 30, 2026 03:24
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