<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: OpenCapture</title>
    <description>The latest articles on DEV Community by OpenCapture (@opencapture).</description>
    <link>https://dev.to/opencapture</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107321%2F4db0a118-996c-49db-a162-a44f03344056.png</url>
      <title>DEV Community: OpenCapture</title>
      <link>https://dev.to/opencapture</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opencapture"/>
    <language>en</language>
    <item>
      <title>Eight things in your frontend that make your app impossible to screenshot</title>
      <dc:creator>OpenCapture</dc:creator>
      <pubDate>Fri, 04 Sep 2026 07:58:50 +0000</pubDate>
      <link>https://dev.to/opencapture/eight-things-in-your-frontend-that-make-your-app-impossible-to-screenshot-39fo</link>
      <guid>https://dev.to/opencapture/eight-things-in-your-frontend-that-make-your-app-impossible-to-screenshot-39fo</guid>
      <description>&lt;p&gt;Someone eventually tries to capture your app. A user filing a bug, a support agent writing a ticket, a PM building a deck, a customer archiving an invoice. They press whatever full-page screenshot tool they have, and they get something wrong: a nav bar repeated nine times, a table with six rows in it, a page that is mostly grey boxes.&lt;/p&gt;

&lt;p&gt;They will not file that as a bug against you. They will just send the broken image, and whoever receives it will misunderstand the problem.&lt;/p&gt;

&lt;p&gt;Full-page capture works by scrolling the document, photographing the viewport at each step, and stitching the slices together. Almost everything below is a case where that loop sees something different from what a human sees.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;position: fixed&lt;/code&gt; and &lt;code&gt;position: sticky&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A fixed header is present in every slice, so it appears once per viewport height in the stitched image. Sticky is the same story with extra edge cases at its stuck/unstuck boundary.&lt;/p&gt;

&lt;p&gt;Good capture tools detach these for the duration and put them back. Not all of them do, and none of them can guess which of your elements is decorative chrome and which is content.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Virtualized lists
&lt;/h2&gt;

&lt;p&gt;This is the one that cannot be worked around from outside.&lt;/p&gt;

&lt;p&gt;If you render 20 of 10,000 rows and swap them as the user scrolls - react-window, TanStack Virtual, ag-grid, your own implementation - then rows 21 through 10,000 &lt;strong&gt;do not exist in the DOM at any moment&lt;/strong&gt;. There is no scroll position at which they can be photographed. A capture of your table is a capture of whatever was mounted.&lt;/p&gt;

&lt;p&gt;Nothing on the tool side fixes this. The page has to offer a non-virtualized rendering path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// something a capture tool or a user can actually reach&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;render=full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;virtualizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;overscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even a "print view" that renders everything is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lazy-loaded images
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt; fires when the image enters the viewport, then the image has to arrive over the network and lay out. A capture loop that scrolls and photographs immediately wins that race and records the placeholder.&lt;/p&gt;

&lt;p&gt;The tool should wait. Many don't, and some can't tell the difference between "still loading" and "intentionally blank".&lt;/p&gt;

&lt;p&gt;Native &lt;code&gt;loading="lazy"&lt;/code&gt; behaves better here than a hand-rolled observer, because browsers already pre-load nearby images and print paths know about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Inner scroll containers
&lt;/h2&gt;

&lt;p&gt;A capture scrolls the &lt;em&gt;document&lt;/em&gt;. A &lt;code&gt;div&lt;/code&gt; with &lt;code&gt;overflow: auto&lt;/code&gt; is a separate scroll context, and it is not the document.&lt;/p&gt;

&lt;p&gt;So the page around it captures fully, and the panel inside it captures whatever slice happened to be visible. Code blocks with &lt;code&gt;max-height&lt;/code&gt;, chat panes, data tables with a frozen header, sidebars - all of them silently truncate.&lt;/p&gt;

&lt;p&gt;If a region is content rather than a viewport-sized widget, consider letting it grow and letting the page scroll instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;100vh&lt;/code&gt; app shells
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.app&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;grid-template-rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a completely normal layout, and it means your document never scrolls. From the outside, your entire application is one viewport tall. A full-page capture of it returns one screen - correctly, because that &lt;em&gt;is&lt;/em&gt; the whole document.&lt;/p&gt;

&lt;p&gt;Everything real is inside &lt;code&gt;.main&lt;/code&gt;, which is case 4.&lt;/p&gt;

&lt;p&gt;I ran into this building a demo page for a capture tool: the page had a sticky sidebar and a viewport-height shell, and captures of it kept coming out with a 1,400px empty column. The page was fine. The layout was just not a document.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;code&gt;scroll-behavior: smooth&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Set globally, it turns every programmatic &lt;code&gt;scrollTo&lt;/code&gt; into an animation. A capture loop that scrolls and immediately photographs gets a frame from the middle of the animation, and every slice lands at the wrong offset.&lt;/p&gt;

&lt;p&gt;Scope it to user-initiated navigation rather than applying it to &lt;code&gt;html&lt;/code&gt; unconditionally, or respect the reduced-motion query, which most capture paths and many users share:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;no-preference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;scroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Scroll-triggered animation
&lt;/h2&gt;

&lt;p&gt;Fade-in-on-scroll means an element's opacity depends on when it was observed. During a fast programmatic scroll, some elements are caught mid-transition and some never trigger at all. The stitched image gets a page with randomly half-faded sections.&lt;/p&gt;

&lt;p&gt;Same reduced-motion escape hatch applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Canvas and WebGL
&lt;/h2&gt;

&lt;p&gt;A WebGL context created without &lt;code&gt;preserveDrawingBuffer: true&lt;/code&gt; may be cleared before anything outside the render loop can read it, so it can come back blank or stale depending on the capture path. If a chart matters to your users, it is worth checking what it looks like in a capture rather than assuming.&lt;/p&gt;




&lt;h2&gt;
  
  
  A test that takes one minute
&lt;/h2&gt;

&lt;p&gt;Open your own app. Use the built-in browser feature - no extension needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome:&lt;/strong&gt; DevTools, Ctrl+Shift+P, type &lt;code&gt;screenshot&lt;/code&gt;, choose &lt;em&gt;Capture full size screenshot&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox:&lt;/strong&gt; right-click the page, &lt;em&gt;Take Screenshot&lt;/em&gt; -&amp;gt; &lt;em&gt;Save full page&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then check four things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Failure looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Header count&lt;/td&gt;
&lt;td&gt;Nav bar appears more than once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image content&lt;/td&gt;
&lt;td&gt;Grey placeholders below the fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long lists&lt;/td&gt;
&lt;td&gt;A table with only the visible rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page height&lt;/td&gt;
&lt;td&gt;The capture is exactly one screen tall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the last row is true, you have case 5 and everything else is moot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Screenshots of your product are how it gets described to people who have not used it - in bug reports, in internal decks, in support threads, in a review someone posts. If those images are broken, the description is broken, and you are not in the room to correct it.&lt;/p&gt;

&lt;p&gt;Fixing this is mostly not new code. It is a print/export path, one query parameter that disables virtualization, and not putting &lt;code&gt;scroll-behavior: smooth&lt;/code&gt; on &lt;code&gt;html&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written while working on &lt;a href="https://opencapture.app" rel="noopener noreferrer"&gt;OpenCapture&lt;/a&gt;, a free and open-source (AGPL-3.0) full-page screenshot extension for Chrome, Firefox and Edge. Source is on &lt;a href="https://github.com/Open-Capture/OpenCapture" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you want to see how any of the above is handled in practice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>frontend</category>
      <category>ux</category>
    </item>
    <item>
      <title>Better bug reports: what to screenshot, and what to hide first</title>
      <dc:creator>OpenCapture</dc:creator>
      <pubDate>Thu, 03 Sep 2026 06:17:42 +0000</pubDate>
      <link>https://dev.to/opencapture/better-bug-reports-what-to-screenshot-and-what-to-hide-first-51li</link>
      <guid>https://dev.to/opencapture/better-bug-reports-what-to-screenshot-and-what-to-hide-first-51li</guid>
      <description>&lt;p&gt;Most bug reports contain a screenshot. Most of those screenshots are either not enough to reproduce the problem, or contain something that should not have left the building.&lt;/p&gt;

&lt;p&gt;Both are fixable in about thirty seconds each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture more than the error
&lt;/h2&gt;

&lt;p&gt;The instinct is to screenshot the error message. The error message is usually the least informative part of the page.&lt;/p&gt;

&lt;p&gt;What the person fixing it needs is context: the URL, the state of the form, what else was on screen, and — often decisive — the browser console. An error alone tells them what broke. The surrounding page tells them why.&lt;/p&gt;

&lt;p&gt;A full-page capture costs nothing extra and removes an entire round trip of "can you send the whole page?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Include the things you are tempted to crop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The URL bar.&lt;/strong&gt; Query parameters and path segments are frequently the difference between reproducing a bug and not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamps.&lt;/strong&gt; If the page shows one, it lets someone match your report against server logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The console.&lt;/strong&gt; Open DevTools before capturing. A stack trace in the image saves a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your viewport size.&lt;/strong&gt; Many layout bugs only exist at certain widths. Some tools stamp dimensions on the capture; if not, mention it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then hide the things you are tempted to leave
&lt;/h2&gt;

&lt;p&gt;Bug reports travel further than the person writing them expects. They get pasted into group chats, forwarded to vendors, attached to public issue trackers.&lt;/p&gt;

&lt;p&gt;Before sending, redact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customer names and email addresses&lt;/strong&gt; visible in tables or headers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account and order identifiers&lt;/strong&gt; that map to a real person&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API keys or tokens&lt;/strong&gt; visible in a logged request or a URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal hostnames&lt;/strong&gt; that reveal infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything in an adjacent browser tab&lt;/strong&gt; — tab titles are readable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Redaction has a failure mode
&lt;/h2&gt;

&lt;p&gt;Two operations are both called blur, and only one of them removes information.&lt;/p&gt;

&lt;p&gt;Some tools draw a blurred overlay while the original pixels remain in the underlying data. Depending on the export, that can sometimes be recovered. Others resample the region and discard the source, which cannot be undone.&lt;/p&gt;

&lt;p&gt;Test yours once: blur a line of text, export, open the export, and push contrast and sharpness hard. If characters start resolving, use a solid box instead of blur for anything that matters.&lt;/p&gt;

&lt;p&gt;Solid boxes are ugly and completely reliable. For a token, take the ugly option.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist worth pinning
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Full page, not just the error&lt;/li&gt;
&lt;li&gt;Console open if it is a JavaScript bug&lt;/li&gt;
&lt;li&gt;URL visible&lt;/li&gt;
&lt;li&gt;Redact names, emails, IDs, tokens, internal hostnames&lt;/li&gt;
&lt;li&gt;Check no other tab is readable&lt;/li&gt;
&lt;li&gt;Note the browser and viewport width&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Six items, about a minute, and it removes most of the back-and-forth that makes bug reports slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I work on &lt;a href="https://opencapture.app" rel="noopener noreferrer"&gt;OpenCapture&lt;/a&gt;, an open-source screenshot extension built around this workflow — full page capture, annotation, and a destructive mosaic blur rather than a cosmetic one. But the checklist above works with whatever you already have, including the capture tool already built into your browser.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
