A Chrome extension for previewing any URL across realistic device viewports — without leaving the browser. Built in vanilla TypeScript on top of WXT, with bundled SVG bezels, a transform-based pan/zoom canvas, and full-page scroll-and-stitch screenshots.
The "RPC" lives on the toolbar. The whole thing is one panel page.
Just want to use RPC, not build it?
- Grab the latest zip from Releases → download
responsive-point-check-<version>-chrome.zip. - Unzip it anywhere on your machine.
- Open
chrome://extensions, toggle Developer mode (top-right), then click Load unpacked and pick the unzipped folder. - Pin the RPC icon from the puzzle menu so it's always one click away.
Open any tab → click RPC → the panel opens in a new tab pre-filled with that page's URL. That's it.
Updates aren't automatic — when a new release lands, download the new zip and click Update on the extension card in
chrome://extensions. The full version history lives in CHANGELOG.md.
| Browser | Status | Notes |
|---|---|---|
| Chrome 88+ | ✅ Tested | Primary target. Everything described in the README works. |
| Edge 88+ | ✅ Works | Chromium-based, MV3 + declarativeNetRequest parity. |
| Brave 1.34+ | ✅ Works | Chromium-based. Brave Shields may extra-block some embeds. |
| Opera 74+ | ✅ Works | Chromium-based. |
| Arc | ✅ Works | Chromium-based. Same Load-unpacked flow. |
| Vivaldi 4.4+ | ✅ Works | Chromium-based. |
| Firefox | Firefox MV3 lacks full declarativeNetRequest parity. Header stripping likely won't fire — sites with X-Frame-Options: DENY won't load. WXT can target Firefox in theory; not yet wired here. |
|
| Safari | ❌ Not supported | Safari Web Extensions use a different extension model + native Xcode wrapper. |
If you've got it working on a browser not listed, open an issue so we can add it to the table.
- One device at a time, properly framed. Every viewport renders inside a real iPhone / iPad / MacBook Air / iMac SVG, not a CSS rectangle. Bezels are bundled (no external CDN, works offline).
- Pan + zoom canvas. Translate-and-scale rendering, zoom-to-cursor on Ctrl+wheel, hold Space (or toggle the rail) to pan. Pan stays clamped so the device can't fly offscreen.
- Realistic browser chrome overlays. iOS Safari's bottom URL pill, Android Chrome's stacked status bar, macOS Safari's traffic-light header — overlaid on the iframe so the screenshot looks like a real device.
- Three screenshot modes. Site only, with mockup, or full-page scroll-and-stitch that drives the iframe via postMessage and writes one tall PNG.
- Custom devices. Type a width × height, save it under "My devices". Recents row sticks the last six picks above the picker tabs.
- Keyboard-first.
Rrotate,[/]cycle devices in category,Ffit,+/-zoom,Ssite shot,Shift+Smockup shot,Dopen picker,1-4jump to category,?help. - Breakpoint guides. Toggle vertical guides at 640 / 768 / 1024 / 1280; they live inside the frame so they pan and zoom with the device, and skip themselves when the viewport is narrower than the breakpoint.
- Auto-strips X-Frame-Options + CSP for sub-frames initiated by the panel only — your normal browsing isn't touched.
Skip this section if you just want to install RPC — see Install — no Node required above.
pnpm installThe postinstall script extracts Geist + Geist Pixel font assets out of the geist npm package into public/fonts/. They're gitignored (regenerated on every install).
pnpm buildThen in Chrome:
chrome://extensions- Toggle Developer mode (top-right)
- Load unpacked → point at
dist/chrome-mv3/
The build is fully self-contained — drag dist/chrome-mv3/ straight into the Load unpacked dialog and you're done. No Node process needs to stay running while you use the extension.
pnpm devWXT prints the unpacked path and opens Chrome with the extension auto-loaded. The panel page is hot-reloaded on every save; the service worker reloads on a manifest change. Use this when you're actively editing — otherwise prefer the static build above.
Click the RPC action icon in the toolbar (pin it from the puzzle menu so it stays visible). The panel opens in a new tab pre-filled with the current page's URL.
You can also navigate directly to chrome-extension://<EXT_ID>/panel.html?url=https://example.com — useful for bookmarking specific previews.
pnpm build
pnpm tsx scripts/test-fixes.tsPlaywright launches a Chromium with the unpacked extension and walks through 19 assertions covering the dNR rule, picker flows, keyboard shortcuts, pan clamp, breakpoint guides, custom-device validation, and the bridge protocol. Exits non-zero on any failure.
pnpm screenshot
# Or with a different URL:
RPC_URL=https://your-site.com pnpm screenshotWalks the whole device catalog and writes one PNG per device into screenshots/. Useful for visual regression sweeps when you tweak bezel layouts.
entrypoints/
├── background.ts # Service worker: dNR header strip, action click, capture relay
├── frame-bridge.content.ts # Injected into every iframe — scroll sync, scrollbar hide,
│ # full-page metrics + scroll-to commands
└── panel/
├── main.ts # Bootstrap, theme + canvas-bg + breakpoint guide application
├── canvas.ts # Pan + zoom (translate3d + scale), wheel/keyboard/pointer gestures
├── frame.ts # One device frame: bezel SVG inline, iframe positioning, chrome
│ # overlays, breakpoint guide rendering, bezel-hide collapse
├── browser-chrome.ts # Fake iOS / Android / macOS browser chrome elements
├── toolbar.ts # URL bar, history datalist, device + zoom labels, reload, theme
├── right-rail.ts # Icon column on the right; site / mockup / full-page captures
├── device-picker.ts # <dialog> with tabs + recents row + "My devices" custom form
├── settings.ts # Toggles, canvas-bg picker, theme, breakpoint guides
├── help.ts # FAQ accordion
├── keyboard.ts # Global keydown shortcut router
├── tooltip.ts # Single shared tooltip driven by data-tooltip attrs
├── store.ts # Module-scope state + chrome.storage.local debounced persistence
├── h.ts # 30-line DOM helper (no JSX, no framework)
└── style.css
lib/
├── devices.ts # Static device catalog + per-device bezel layout overrides
├── bezel-asset.ts # Bezel SVG cache + per-kind padding ratios + layout builder
└── icons.ts # Inline SVG strings for the rail + chrome buttons
public/
├── bezels/ # Per-device + per-kind SVG artwork (iPhone 17 Pro, iPad,
│ # MacBook Air M4 Sky Blue, iMac, Pro Display XDR, etc.)
└── fonts/ # GENERATED by postinstall — Geist + Geist Mono
scripts/
├── extract-geist-assets.mjs # Copies Geist woff2s out of the npm package
├── screenshot-devices.ts # Playwright walks DEVICES, screenshots each .frame
└── test-fixes.ts # Playwright E2E asserts the audit fixes
-
Drop the SVG into
public/bezels/<id>.svg(must be pure vector — embedded raster PNGs inside an SVG wrapper won't render correctly). -
Add an entry to
DEVICESinlib/devices.ts:{ id: 'iphone-17-pro', name: 'iPhone 17 Pro', category: 'apple-phone', width: 402, height: 874, dpr: 3, bezel: 'phone-island', bezelSrc: 'bezels/iphone-17-pro.svg', bezelLayout: { outerWidth: 440, outerHeight: 908, screen: { x: 19, y: 19, width: 402, height: 874, radius: 56 }, }, }
- Without
bezelSrc→ the device uses thebezelkind's default artwork atpublic/bezels/<kind>.svgand the kind's default padding fromBEZEL_PADDINGinlib/bezel-asset.ts. - With
bezelSrc+bezelLayout→ exact pixel positioning inside your custom artwork. Coordinates are in the SVG's own viewBox space.
- Without
-
Reload the panel — the picker rebuilds from
DEVICESon every mount.
Some sites enforce iframe blocking via JS (if (window.top !== window) location.replace(...)), not just headers. RPC strips X-Frame-Options + CSP frame-ancestors, but it can't undo client-side framebusting. Open the URL in a regular tab and screenshot from there as a workaround.
Cookies with SameSite=Lax flow through automatically — sign in to the site in a regular tab in this Chrome profile and reload the panel. Cookies with SameSite=Strict are blocked inside iframes by Chrome's cookie policy. There's no workaround within the extension.
Some shells terminate WXT when launched via certain background mechanisms. Run it detached:
nohup pnpm dev > /tmp/wxt.log 2>&1 < /dev/null &
disownOr run it in a regular terminal foreground.
The bridge needs to drive window.scrollTo inside the iframe — sites that pin the URL to a router-managed scroll position (or use virtualization) may snap back. The capture downloads whatever was stitched up to that point.
The artwork's natural aspect ratio doesn't match the device viewport's aspect, so the SVG is being non-uniformly stretched. Either:
- Add a
bezelLayoutoverride with the exactouterWidth × outerHeightmatching the artwork, OR - Provide a per-device SVG via
bezelSrcshaped for that viewport.
- The panel HTML + JS + CSS is under 50 KB before fonts and bezels.
- Zoom updates run in a single
transform: translate3d() scale()write per frame — no layout invalidation. - Bezels are inlined SVG so
currentColorinherits the theme without re-fetching artwork on theme switch. - State writes to
chrome.storage.localare debounced 300 ms — rapid switching coalesces into one write. - The frame-bridge content script stays inert until the panel posts
rpc:init— no per-frame cost on normal browsing.
- WXT — Vite-based MV3 framework (manifest generation + HMR + zip).
- TypeScript strict —
exactOptionalPropertyTypes,noUncheckedIndexedAccess,noImplicitOverride. Noany. - Geist — single typeface across the whole UI; Geist Mono in
<input type="url">. - Playwright — drives the device-screenshot + audit-fix test suites.
- No UI framework. Plain DOM via a 30-line
h()helper.
MIT.
