A browser extension that keeps a list of sites you don't want to visit and covers them with a full-screen, undismissable blurred overlay the moment a tab lands there. The page still loads behind the blur — this is a cover, not a redirect — which is what keeps the whole feature to one content script, with no background service worker and no navigation-interception permissions.
The overlay, the popup, and an on-page theme picker all share one user-selected theme (a plain white default, plus 188 palettes lifted from Monkeytype), and the overlay can optionally play looping CRT static behind its wordmark.
- Node.js
^20.19.0 || >=22.12.0— runnvm useto pick up the pinned version in.nvmrc. - A Chromium-based browser (Chrome, Edge, Brave, Arc, …) to load the extension in.
See COMPATIBILITY.md for exact versions this is
built and tested against, including the browser-side CSS feature that sets
the real minimum Chromium version.
git clone https://github.com/farthestmage/EXTENSION_BLOCKER.git
cd EXTENSION_BLOCKER
nvm use # or otherwise switch to a Node version in package.json#engines
npm install
npm run devThen, in Chrome:
- Go to
chrome://extensions. - Turn on Developer mode (top right).
- Click Load unpacked and select the
dist/folder this repo builds to.
npm run dev rebuilds dist/ on save; the extension picks up most changes
after a manual reload from chrome://extensions (a content-script change
also needs the affected tab reloaded).
npm run buildType-checks (tsc -b), builds to dist/, and zips it to
release/crx-extension-blocker-<version>.zip — the artifact you'd upload to
the Chrome Web Store or hand to someone to load unpacked.
manifest.config.ts MV3 manifest (name/version pulled from package.json)
vite.config.ts Vite + @crxjs/vite-plugin + zip-pack wiring
src/
content/
main.ts The overlay: shadow root, blocking logic, CRT paint,
background media, message listener for the picker
picker.ts The theme picker as it renders on the page (lazy-loaded)
popup/
App.tsx Toolbar popup: block list, add/remove/enable, theme menu
ThemePicker.tsx React fallback picker, used only where a content
script can't be reached (chrome://, Web Store, …)
lib/
storage.ts Block list persistence (chrome.storage.local)
match.ts Hostname normalization + blocked-host matching
theme.ts Active theme persistence + color/contrast helpers
media.ts Paths/volume/opacity for the optional CRT footage
tokens.css, crt.css Shared design tokens and the CRT effect layer
picker.css One stylesheet shared by both theme pickers
constants/themes/ Generated palette data — see below, don't hand-edit
scripts/
gen-themes.mjs Regenerates src/constants/themes/ from Monkeytype
prep-media.sh Turns raw footage into the two files the overlay loads
yt-download.py Optional helper to fetch that raw footage
For the why behind these choices (why an overlay and not a redirect, why a
closed shadow root, why the CRT effect is built the way it is, the animation
rules, etc.) see CLAUDE.md — it's the living architecture
doc for this project, not just Claude-specific instructions.
Themes live in src/constants/themes/: one <name>.ts + one <name>.css
per palette, generated — not hand-edited — from Monkeytype's own theme
table at a pinned commit:
node scripts/gen-themes.mjsThis wipes and regenerates the whole directory, so a palette Monkeytype removes upstream doesn't leave a stale file behind.
The blocked-site overlay can play a looping CRT-static video behind the wordmark, with a matching hiss underneath it. Both files are gitignored, so a fresh clone builds and runs fine without them — the overlay simply renders without the footage, exactly as it did before the feature existed.
To enable it, drop your own files at:
public/media/blockix-static.mp4 # muted, looping, object-fit: cover
public/media/blockix-static.mp3 # looping, pre-cut to its own loop point
Requirements:
- The mp4 must have no audio track. The mp3 owns the sound, which is what lets the two loop at different lengths. H.264 / yuv420p, 1080p30 or smaller.
- The mp3 must already be cut to its loop point. Playback restarts at 0 and never seeks, so any intro you leave in replays every cycle.
- Keep both small. Static is incompressible noise; a long 1080p clip
runs to hundreds of MB and would ship in the release
.zip.
bash scripts/prep-media.sh produces both from raw captures in
scripts/downloads/ (fetch those with scripts/yt-download.py):
brew install ffmpeg
pip install yt-dlp
bash scripts/prep-media.sh # uses the default source filenames
bash scripts/prep-media.sh in.mp3 in.mp4 # or bring your ownPaths, loop volume, and the per-theme opacity live in src/lib/media.ts;
the styling lives with the other CRT strata in src/lib/crt.css under
.crt-media.
main— stable, released.dev— day-to-day development; branch new work off here.devmerges intomainto publish a release.
-
Theme palettes — the 188 non-default themes are Monkeytype's, pulled verbatim by
scripts/gen-themes.mjsfrommonkeytypegame/monkeytypeat a pinned commit. All credit for the palette design goes to Monkeytype and its contributors; none of those colors originate in this project. -
CRT static footage (optional, gitignored, not distributed in this repo) — the default source clips referenced by
scripts/prep-media.share:- "Analog CRT TV Static / White Noise for ASMR, Sleep, Studying, Poltergeists, Etc. (1985 Hitachi)" (audio)
- "TV STATIC (4K 60FPS)" (video)
Anyone regenerating the footage with different sources should credit those instead — see
scripts/yt-download.pyandscripts/prep-media.sh. -
Font — Roboto Mono, bundled via
@fontsource/roboto-mono. -
Icons — lucide-react.
-
Built with Vite, @crxjs/vite-plugin, and React.
MIT.