Inspiration
Most dark-mode or seasonal browser extensions either break complex modern websites, demand invasive <all_urls> permissions at install time, or look like generic "AI slop" packed with neon glows and gradients. For Kiroween 2026's Costume Contest, we wanted to dress up the entire web like an old book read by candlelight on a foggy night—while keeping every page readable, interactive, and 100% private.
What it does
Haunt Any Page is a zero-dependency Manifest V3 Chrome extension that transforms any webpage with a single click:
- Candle-lit palette: Turns light pages into bone-on-charcoal parchment while counter-filtering photos and videos so media stays natural. Already-dark sites (like GitHub Dark, Discord, or MDN) are automatically detected via WCAG relative luminance (
< 0.2) and gently tinted rather than inverted. - Flying bats: Six SVG bats with articulated two-joint wings (shoulder, elbow, wrist, and scalloped fingertips) driven by a real-time flight simulation—complete with lift on each downstroke, erratic insect-chasing darts, pairwise separation, and a 3×2 viewport grid so they never bunch into a flock.
- Old-book typography: Applies bundled IM Fell DW Pica to prose and headings while excluding
code,pre, form controls, SVGs, and icon fonts. - Candle flicker: A subtle
steps(1)edge vignette that dims by at most 30% and steps at most 3 times per second. - Independent Weather (Fog): Three layers of seamless periodic 5-octave fractal value noise rendered into
<canvas>tiles that drift at independent speeds. Fog has its own Density, Speed, and Height sliders, adapts its palette to light or dark pages, and works on any page—even when the haunted skin is off. - Instant, clean restore: A second click removes every class, font face, and overlay element so
document.documentElement.outerHTMLmatches the original DOM. - Remember this site: Uses
optional_host_permissionsper origin (chrome.permissions.request) andchrome.scripting.registerContentScriptsatdocument_startonly for sites the user explicitly chooses.
How we built it (and how Kiro was used)
We built the entire project in Kiro IDE using its spec-driven workflow, steering documents, and automated PostFileSave hooks (see docs/kiro-usage.md for the full write-up):
Specs (
.kiro/specs/haunt-any-page/):requirements.md: Defined EARS acceptance criteria (WHEN ... THE SYSTEM SHALL ...) covering one-click haunt/restore, effects, independent Weather fog, per-site memory, zero-network privacy, and WCAG AA accessibility.design.md: Specified the three-part MV3 architecture (popup, content script, service worker), the exact 5-octave periodic noise math, the articulated bat wingbeat geometry, and closed Shadow DOM isolation withadoptedStyleSheets.tasks.md: Broke the build into 9 ordered tasks gated bynpm testandnpm run check.
Steering (
.kiro/steering/):product.md: Locked scope to a fast, dependency-free MV3 extension with zero external requests.visual-style.md: Enforced strict "No AI slop" design rules (flat#161412surface,#ece6d8bone ink,#d4553fwax-seal accent, left-aligned layout, Grenze Gotisch wordmark + Atkinson Hyperlegible body, zero neon glows or decorative gradients).extension-rules.md: Enforced minimal permissions (activeTab,scripting,storage), closed Shadow DOM,createElementNS(noinnerHTML), andchrome.storage.localonly.
Agent Hooks (
.kiro/hooks/): ThreePostFileSavehooks ran automatically on every save:check-manifest.json→scripts/check-manifest.mjs: Rejects MV2 fields, broad permissions (tabs,<all_urls>,webRequest), missing asset files, or unsafeweb_accessible_resources.check-contrast.json→scripts/check-contrast.mjs: Parses CSS rules and verifies every foreground/background pair meets WCAG AA (>= 4.5:1).check-privacy.json→scripts/check-privacy.mjs: Scanssrc/to blockfetch,XMLHttpRequest,WebSocket,sendBeacon,eval,new Function,chrome.storage.sync, and remote URLs.
Challenges we ran into
- Strict Content Security Policies (CSP) on sites like GitHub and YouTube block inline
<style>tags and data URLs. We solved this by attaching closed shadow roots (mode: 'closed'), styling them vianew CSSStyleSheet()+shadow.adoptedStyleSheets, drawing fog directly to<canvas>elements, and loading bundled fonts via theFontFaceAPI. - Seamless fog wisps: Wrapping coordinates (
x % tileWidth) created a visible vertical seam as fog drifted. Wrapping the noise lattice period itself (period = scale * 2^octave) produced seamlessly tiling wisps across a 200%-wide canvas. - Dark-page race conditions at
document_start: When auto-haunting a remembered site atdocument_start,<body>and page stylesheets aren't loaded yet, making dark sites appear transparent. We added tone re-checks onDOMContentLoaded,load, and post-load while temporarily ignoring our ownhaunt-onroot class during sampling.
Accomplishments that we're proud of
- Zero-trace restoration: Verified by an automated Puppeteer E2E test (
npm run test:e2e) that comparesdocument.documentElement.outerHTMLbefore haunting and after restoring. - Crafted popup UI: A quiet, utilitarian tool that passes automated WCAG AA contrast checks (
check-contrast.mjs) and avoids every cliché of generic AI UI generation. - Zero network activity: Everything—fonts, icons, noise generator, bat physics—is bundled locally and verified by
check-privacy.mjsand Puppeteer request interception.
What we learned
Combining Kiro's EARS requirements with executable PostFileSave hooks turns architectural rules (like WCAG AA contrast and zero-network privacy) from aspirational guidelines into hard invariants that are verified on every single file save.
Try it (for judges)
- Download
haunt-any-page-v0.1.0.zipand unzip it (or clonehttps://github.com/phasepoemshield/haunt-any-page.git). - Open
chrome://extensions/in Chrome, Edge, or Brave. - Enable Developer mode (top-right toggle).
- Click Load unpacked and select the extracted folder.
- Open any
http/httpswebpage (e.g.,https://en.wikipedia.org), click the Haunt Any Page toolbar icon, and click Haunt this page or toggle Weather → Fog.
Built With
- canvas
- chrome
- css3
- html5
- javascript
- kiro
- manifest-v3
- puppeteer
- shadow-dom
- web-animations-api