A dark-mode bookmarklet for Hacker News
What it does
- Dark theme tuned for OLED iPhone screens
- Survives navigation: tap once on HN, then click stories, comments, and pagination — dark mode stays on without re-tapping
Install on iPhone (bookmarklet)
iOS Safari won't let you save a javascript: URL directly from the
address bar, so use the bookmark-then-edit dance:
- Copy the contents of
dist/hn-dark.bookmarklet.txt. - In Safari, open any page → share icon → Add Bookmark. Save it.
- Tap the bookmarks icon → Edit → tap the new bookmark.
- Rename to HN Dark. Tap the URL field, clear it, paste the entire
contents of
hn-dark.bookmarklet.txt. Save. - Move it to the Favorites folder so it appears in the start-page grid when you tap the address bar.
Use it: navigate to news.ycombinator.com, tap the address bar, tap
the HN Dark favorite. Browse normally — dark mode persists across
HN navigation.
Project layout
src/
hn-dark.js # bookmarklet entry; references __HN_DARK_CSS__
hn-dark.css # styles, with CSS variables for easy tuning
build.mjs # esbuild driver
dist/
hn-dark.bookmarklet.txt # the javascript: URL (build output)
hn-dark.min.js # same payload without the prefix
Build
npm install npm run build # writes dist/hn-dark.bookmarklet.txt npm run watch # rebuild on src/ changes
The build pipeline:
- esbuild minifies
src/hn-dark.cssto a string. - esbuild minifies
src/hn-dark.js, replacing the__HN_DARK_CSS__identifier with the minified CSS string viadefine, and wraps the whole thing in an IIFE. - The output is prefixed with
javascript:and saved todist/hn-dark.bookmarklet.txt.
Why one tap from anywhere isn't possible (with a bookmarklet)
The bookmarklet only does anything on news.ycombinator.com — invoked
elsewhere, it's a no-op. It can't redirect-then-style: a top-level
navigation destroys the bookmarklet's JS context, and HN's
frame-ancestors 'self' blocks iframing it from a non-HN parent.
A truly one-tap-from-anywhere solution requires a Safari extension (more work + Apple developer fee $), or a hosted reverse proxy (untrustworthy).
Why the iframe approach works
- HN sends
Content-Security-Policy: ... frame-ancestors 'self', which per spec overrides itsX-Frame-Options: DENY. So HN can frame itself. - CSP
style-src 'unsafe-inline'allows the injected<style>tag. - Same-origin parent + iframe gives full DOM access; cookies and
fnidform tokens flow normally, so login/voting/commenting all work.