A lightweight browser extension that expands an image or video next to your cursor when you hover over its thumbnail — and plays YouTube/Twitch videos inline on hover. Works in Chromium (Chrome/Edge/Brave) and Firefox from a single Manifest V3 codebase.
- Hover to expand — the full-size media appears beside the cursor and follows it, repositioning to stay on screen.
- Hover to play video — hovering a YouTube or Twitch link/thumbnail opens an
autoplaying, muted player; direct
.mp4/.webm/.gifmedia plays too. - Reddit videos & galleries — hovering a v.redd.it post plays the video; hovering a gallery post shows it with an image count, and the arrow keys step through it.
- Center to inspect — press the pin key (default
Space) to sticky-center the preview for a focused look. Press again to close. - PiP / free-flow — press the PiP key (default
P) to detach the preview into a floating window you can drag to move and keep on-screen while you scroll and browse the page underneath. Switching in from center or hover is seamless (no reload). Close it with the close key or by pressing the PiP key again. - Zoom, pan & resize —
+/-zoom in both center and PiP; drag the bottom-right corner to resize. In center mode the scroll wheel zooms, and once you're zoomed in past the viewport, moving the cursor pans around the image so you can inspect any part. A hover preview is dismissed on scroll; under a PiP the page scrolls normally. - Close — press the close key (default
Esc), or click outside the preview (except in PiP, so page clicks keep working). - Right-click to save — right-click an image/video preview to download it.
- Smart resolution — generic heuristics (
srcset, wrapping<a>links, lazy-loaddata-*attributes, CSSbackground-image), plus a data-driven rule set (shipped as editable defaults) covering YouTube, Twitch, Twitter/X, Imgur, and Wikimedia/Wikipedia. The resolver itself is site-agnostic; all host knowledge lives in rules. (Reddit videos/galleries are the one exception — they need async post-JSON fetching and shadow DOM traversal, so they stay in a dedicated resolver.)
| Detached picture-in-picture | Zoomed in to inspect |
|---|---|
![]() |
![]() |
Install from the Chrome Web Store or Firefox Add-ons if you just want to use it. To build it yourself:
npm install
npm run build # outputs the loadable extension to dist/
npm run watch # rebuild on change during developmentIcons are checked in under src/icons/.
Chromium: go to chrome://extensions, enable Developer mode, click Load
unpacked, and select the dist/ folder. (Enable Allow access to file URLs to test
with test/fixtures.html.)
Firefox: go to about:debugging#/runtime/this-firefox, click Load Temporary
Add-on, and select dist/manifest.json.
The bundle is built with esbuild; formatting, linting, and unit tests use Prettier, ESLint, and Vitest.
npm run check # format check + lint + typecheck + tests — run before pushing
npm run format # apply Prettier (format:check to only verify)
npm run lint # ESLint (lint:fix to autofix)
npm run typecheck # tsc --noEmit
npm run test # Vitest unit tests (test:watch to watch)A Husky pre-commit hook runs Prettier and ESLint on staged files via lint-staged, so committed code stays formatted and lint-clean.
Click the toolbar icon for the settings popup. Configurable: the pin, PiP, and close
keys (click a button and press any key to rebind), close-on-click-outside, keep-PiP-
across-navigation, hover delay, minimum image size, and video previews. The close key
is always bound, so the preview can never become impossible to dismiss. Zoom (+/-)
and drag-to-resize are fixed defaults. Changes apply live to open tabs.
PiP persistence across in-page navigation depends on how the site rebuilds its DOM, not on this extension; a full page reload always resets it.
Custom rules are the resolution engine: the built-in site behavior ships as the default rule set, and you can edit it. Each rule rewrites the URL a preview resolves to; rules match in order and the first hit wins.
Because the toolbar popup closes when it loses focus (which makes pasting awkward), the rule editor lives in the full settings tab: the popup shows an Edit custom rules in a tab → button that opens it. In the tab, click + Add rule to add a card and fill in its fields.
| Field | Meaning |
|---|---|
| host | Domain of the media URL to match (includes subdomains), e.g. youtube.com. |
| match | Regex tested against the full URL. Anchor it (^…$) to rewrite the whole URL. |
| replace | Replacement string; $1, $2, … are capture groups; {{host}} is the page's host. |
| type | Auto infers from the result URL; or force Image, Video, or Frame. |
| pageHost | Optional: only apply on pages of this host. Used to give a site page-specific behavior. |
| disable | Optional: when a match hits, suppress the preview entirely (no replace needed). |
The match/replace follow JavaScript String.replace semantics. pageHost, {{host}},
and disable handle the cases that need to know where you are:
- On youtube.com the embed player can't run (YouTube blocks embedding itself, error 152-4) and can't be worked around, so a page-scoped disable rule suppresses the preview there and lets YouTube's own hover autoplay take over. Off YouTube, the nocookie embed player is used.
- Twitch's
parent={{host}}requirement is filled from the page host.
You can add your own disable rules to stop previews on any site that annoys you. Export / Import save and load the rule set as JSON, and Reset to default restores the shipped set.
Unit tests (Vitest, npm test) cover the pure logic — safe URL parsing, the rule
engine and default rules (YouTube on/off site, Twitch, image CDNs), rule
parsing/validation, key-label formatting, and a resolveTarget matrix that exercises
each popular site and media
type end-to-end against DOM fixtures. Layout and zoom behavior depends on real element
geometry, so verify those manually in the browser.
For manual testing, open test/fixtures.html for offline samples covering each
resolution path (anchor-wrapped, data-src, srcset, background-image), plus
network-dependent sections for video files, live site rules, and YouTube/Twitch embeds.
Note on embedded players: YouTube/Twitch previews are shown in an <iframe>, so a
page's Content-Security-Policy can block them on some third-party sites.
YouTube refuses to render its /embed/ player inside a youtube.com page (it fails
with "video unavailable", error 152-4) and there is no way around it — with a
youtube.com referrer it returns 152, without one it returns 153. So on youtube.com the
preview is suppressed (a page-scoped disable rule) and YouTube's own hover autoplay
takes over; off YouTube the nocookie embed player is used. This is expressed entirely
with default rules — see DEFAULT_RULES in src/settings.ts.
src/
manifest.json Cross-browser MV3 manifest
api.ts browser/chrome wrapper + message types
settings.ts Settings + DEFAULT_RULES (the built-in rule set) + storage
background.ts Cross-origin download & fetch handler
content/
index.ts Hover detection & event wiring
resolver.ts Generic candidate collection + rule engine (no site logic)
reddit.ts Reddit video & gallery resolution (async; the one exception)
url.ts Safe URL-parsing helper
viewer.ts Shadow-DOM preview (image / video / iframe)
options/ Options page
build.mjs esbuild bundler
test/ Vitest unit tests + fixtures.html


