Frontend
Capturing exact DOM elements in a Chrome MV3 extension: DPR, sticky elements, and redaction before pixels exist
Mustafa Bahaa DEV Community
1 views
Every screenshot extension workflow I've used shares the same flaw: you take
a picture of the viewport, then crop. The capture is never of the thing you
wanted — it's of a rectangle that happens to contain it.
I spent the last months building Snaptura,
a Chrome MV3 extension that captures exact DOM elements and turns them into
polished exports. This post walks through the four problems that actually
took engineering — skip the marketing, steal the techniques.
1. Capture at the device's resolution, not the CSS fantasy
t.captureVisibleTab() gives you a bitmap of the viewport. The trap: a
400×300px card on a laptop with devicePixelRatio: 2 is not 400×300 in
that bitmap — it's 800×600, and the browser helpfully encodes the DPR into
the PNG so naive consumers render it double-sized or blurry.
The fix is boring and essential: every capture carries its DPR, and every
crop rectangle is multiplied before slicing:
const dpr = window.devicePixelRatio;
const rect = element.getBoundingClientRect();
// Slice the viewport bitmap in *device* pixels:
const slice = {
x: Math.round(rect.x * dpr),
y: Math.round(rect.y * dpr),
width: Math.round(rect.width * dpr),
height: Math.round(rect.height * dpr),
};
// …and remember `dpr` so exports stay 1:1 sharp.
Rounding matters. A Math.floor on the wrong edge gives you a 1-device-pixel
seam of the neighboring element — invisible in code, very visible in a
marketing screenshot.
2. Hover-select is a hit-testing problem
"Select the element under the cursor" sounds like document.elementFromPoint
— until you try it on real pages with overlays, shadow DOM, and iframes. What
worked in the end is closer to what DevTools does:
Walk up from the hovered node and offer the meaningful ancestors, not
just the immediate one (users almost never want the inner <span>; they
want the card).
Score candidates by visible area and semantic weight — a <button> inside
a <div> should be grabbable as either, with a keyboard shortcut to walk
up the chain.
Paint the selection overlay in a page-level layer that ignores
pointer events, so highlighting never changes the page you're capturing.
The heuristic I landed on: the element people mean to select is usually the
nearest ancestor whose background differs from its parent's. Cheap, wrong
occasionally, but predictable in a way users learn in ten seconds.
3. Full-page captures and sticky-element ghosts
Scroll-and-stitch full-page capture has an obvious implementation: scroll by
a viewport height, capture, repeat, then blend the overlap. Two real-world
ruinizers:
Sticky elements stamp themselves over every fold. The fixed header looks
fine in fold one and photobombs folds two through nine. The fix is to detect
position: sticky/fixed elements before scrolling and, per fold, either
hide them or pin their correct-at-this-scroll-offset version. Detection is a
getComputedStyle walk; the subtle part is compositing them back at their
natural position in the final fold, where users expect to see the header.
Scrolling isn't pixel-exact. Page zoom, fractional scroll positions, and
scroll-behavior: smooth all break naive stitching. Forcing
scrollTo({ top: y, behavior: 'instant' }) and waiting for both the scroll
event and a rAF after it eliminated 95% of the seam artifacts.
4. Redact before the pixels exist
Pixel-level redaction (draw a black box over the region) leaves the secret
in the file — metadata strips help, but screenshot tools that blur and undo
are a meme for a reason.
The alternative: redact the DOM itself. Replace the text node with a
placeholder element of identical box metrics, capture, then restore the
original node:
function redact(node) {
const box = node.getBoundingClientRect();
const veil = document.createElement('div');
Object.assign(veil.style, {
position: 'fixed',
left: `${box.x}px`, top: `${box.y}px`,
width: `${box.width}px`, height: `${box.height}px`,
background: 'currentColor', opacity: '0.85',
});
node.hidden = true;
document.documentElement.appendChild(veil);
return () => { veil.remove(); node.hidden = false; };
}
Since the sensitive string is never rasterized, it can't leak — not in the
PNG, not in an undo stack, nowhere. The cost is that redaction must be
declared before capture, which turns out to be a fine UX: you mark regions
once, and every future capture of that page respects them.
MV3 logistics worth knowing
The service worker dies constantly. Any capture pipeline that assumes a
long-lived background page breaks. We keep state in chrome.storage, treat
the worker as a router, and do the heavy lifting elsewhere.
Offscreen documents are the escape hatch. GIF encoding and video
compositing run in an offscreen document with OffscreenCanvas + Workers —
keeping the service worker free and alive.
tabCapture for viewport recording requires the user gesture and the
permission choreography to line up exactly; get that right once and
recording "just works."
That's the core of it. If you want to see the end result of these
techniques, the extension is
Snaptura — free, watermark-free, with a
Pro tier and a lifetime option. But the four patterns above stand on their
own for anything you're building with captureVisibleTab.
What did I miss? If you've shipped capture tooling, I'd love to hear which
part hurt most.
Read original: https://dev.to/mustafa_bahaa/capturing-exact-dom-elements-in-a-chrome-mv3-extension-dpr-sticky-elements-and-redaction-before-1hgd
Related
L
Lumiana — building personal apps with browser and Node.js APIs together
Frontend
0
DEV Community
Y
Your Hook Is Configured Correctly and Never Runs
Frontend
0
DEV Community
H
Halis: The Side Project That Became My Life's Largest Ambition
Frontend
0
DEV Community
B
Building Beyond CRUD with Claude and Codex: How we Created an Interactive Digital Museum
Frontend
0
DEV Community
Comments0
No comments yet — be the first