Capturing DOM as Image Is Harder Than You Think
Client

Capturing DOM as Image Is Harder Than You Think

Anastasiya Kharuk
Anastasiya Kharuk

Dashboards are one of the most essential tools in monday.com. They enable our users to bring together data from multiple boards and use widgets to create visual insights, helping teams track progress, spot trends, and make better decisions. 

But while dashboards are great for internal visibility, sharing these insights externally with stakeholders and execs was still a manual, time-consuming, and frustrating process: users had to take screenshots, manually write context, and stitch everything together.

We wanted to make that easier. So we started working on a way to help users create clear, visually appealing reports within monday.com. Our goal was to make reporting almost effortless and remove all manual work from our users.

One-Click Reporting: Turning Dashboards into Docs

Our idea was to allow users to convert a dashboard into a monday doc in one click. Initially, we considered embedding live, frozen widgets, but building the needed APIs and architecture would’ve taken over 30 developer sprints, which wasn’t feasible for a first release. So we opted for a faster path: capturing each widget as an image directly from the page. These images, paired with AI-generated summaries, are automatically saved into a monday doc — making it easy to annotate and share.

In this post, I’ll walk you through our journey of implementing a reliable DOM-to-image capture feature. I’ll compare the libraries I explored (html2canvas, dom-to-image-more, modern-screenshot) and share the challenges we faced along the way, as well as how we got everything working smoothly.

It was supposed to be easy

When we first set out to implement DOM-to-image capture for our dashboard widgets, we assumed it would be simple: pick a library, call a function, and that’s it. But reality hit differently.

Our dashboard hosts a diverse range of widgets, including charts, calendars, batteries, numbers, tables, Gantt charts, time tracking tools, and more. 

Charts are created using the Highcharts library, which has a built-in solution for exporting them as an image, and it works quite quickly. It took around 2-3 seconds for 10 widgets. While the chart widgets were straightforward, the rest required a custom approach.

First Try: html2canvas

When you search for DOM-to-image solutions, html2canvas is the first recommendation you’ll find. It’s a mature library that renders a DOM node to a canvas using computed styles and layout traversal. Once rendered, you can export the canvas using canvas.toDataURL() or canvas.toBlob().

html2canvas doesn’t take a screenshot. Instead, it mimics what the browser does:

  • Reads computed styles of every DOM element, determining their visual appearance (e.g., size, color, font, borders)
  • Recursively traverses child elements
  • Draws the result manually onto a <canvas>

At first glance, html2canvas seemed perfect for the job. It was easy to set up and produced decent results with simple, static content. We decided to try this approach, and guess what? The performance left much to be desired.

Capturing 10 widgets took over 21 seconds — way too slow for our use case, especially since dashboards can have up to 30 widgets. 

html2canvas works well for simple, occasional client-side snapshots. However, it slows down with bulk captures because each capture is a CPU- and memory-intensive operation, blocking the main thread and consuming resources.

Next Attempt: dom-to-image-more

Taking into account our difficulties with html2canvas, we decided to check dom-to-image-more, an updated version of the initial dom-to-image library. We specifically chose the forked version over the original because it addressed many long-standing issues that were never resolved in the original repository.

Instead of using canvas, it converts the DOM to an SVG snapshot, improving performance and preserving styles more accurately (especially for things like flexbox, shadows, and web fonts).

Results were much better: the same 10 widgets were captured in about 7 seconds. But we ran into new issues:

  • Sometimes it failed silently and got stuck without the ability to handle the error to proceed.
  • In some screenshots, scrollbars appeared even when the original widget had none.
  • In other cases, parts of the widget content were cut off or missing from the image.

These limitations led us to explore community discussions and GitHub issues. We found out that dom-to-image-more heavily relies on accurate DOM cloning. A significant obstacle arises when DOM nodes contain images loaded from external sources, due to CORS (Cross-Origin Resource Sharing) restrictions. Browsers block canvas/image operations if any image in the DOM is loaded from a domain that doesn’t explicitly permit your origin.

Hack: Inlining Images as Data URLs

Unfortunately, this issue affected all libraries we tested. To work around this, we added a preprocessing step to ensure the image is “inline” and same-origin is used:

  1. Image Collection: Find all <img> elements
  2. For each image:
    • If the image source is a data URL or from our domain, we leave it as is
    • If it’s from an external domain, we attempt to fetch the image manually
    • If fetching succeeds, we draw it onto a temporary canvas and convert it to a base64-encoded data URL
    • If fetching fails (due to CORS restrictions), we replace the image with a transparent 1×1 pixel placeholder
  3. Inline Replacement: Replace the src of each image with the base64 data URL (or placeholder)

The library provides a method to manipulate the node before cloning. At this stage, we swap the image sources with these “safe” inline versions. As this method only supports synchronous operations, all image processing had to be completed beforehand.

While we resolved silent failures, the library’s internal rendering pipeline was difficult to debug and lacked transparency in failure scenarios. Even with our custom fixes in place, dom-to-image-more wasn’t robust enough to support consistent, large-scale dashboard exports — and that led us to explore better alternatives.

When everything finally clicked

Our final (and most successful) approach came from trying modern-screenshot, a relatively new library that offered:

  • More robust rendering
  • Better CSS support, improved handling of fonts and SVGs
  • Support for both SVG and canvas exports, depending on the export types

Fun fact: modern-screenshot is a fork of html-to-image, which itself is a fork of dom-to-image.

We didn’t just use it as-is, as handling cross-origin content still requires pre-processing. We wrapped its domToBlob API, adding our own error handling and CORS image handling that we used for dom-to-image-more. Also, if the conversion failed, we replaced the image with a placeholder to avoid breaking the export.

Performance-wise, it gave us around 7 seconds for the same 10 widgets.

DOM-to-Image Libraries comparison table

Bonus hack: When Canvas breaks our screenshot — and how we fixed it

Tainted canvases were another major blocker. These occur when a <canvas> element includes content (such as images or videos) from another origin without proper CORS headers. In such cases, the browser marks the canvas as “tainted,” making it impossible to read or export its content — any attempt to access the image data results in a security error.

To avoid this, we proactively detected and removed tainted canvases from the DOM before running the export logic. We created a utility that tests each <canvas> element and removes unsafe ones, replacing them with placeholders if necessary. While not always visually perfect, it significantly improved reliability.

Known issues we still face

Despite our progress, a few limitations remain that are difficult to address with current client-side solutions fully:

  • Scrollable widgets: Most libraries capture only the visible content, which means long tables or scrollable lists may be cut off.
  • Iframes: Any content inside <iframe> elements cannot be captured because browsers block access to content from other websites for security reasons. These elements will appear blank, and there’s no reliable client-side workaround.

Solving these likely requires server-side rendering or headless browser solutions, which come with their own complexity.

Looking back on the journey

Capturing DOM nodes as images in a complex dashboard is not trivial. It requires a deep understanding of how browsers handle rendering, images, CORS, and the DOM. 

Through trial and error, we landed on a hybrid approach.

  • Use built-in export features where available (e.g., Highcharts)
  • Use modern-screenshot for all other widgets
  • Preprocess images to avoid CORS errors and tainted canvases
  • Implement fallbacks to keep the export smooth and predictable

Today, users can go from dashboard to doc in one click — no more screenshots, no more manual summaries. We’ve made it dramatically easier to share insights across teams and with leadership. And we’re just getting started.

What we’d do differently

If we had to start over, we’d spend more time upfront defining the requirements and constraints of DOM-to-image capture in the context of dashboards. Knowing the performance and compatibility trade-offs earlier would’ve saved us time. We also underestimated the role of CORS.

For others on similar journeys:

  • Never underestimate the complexity of DOM rendering.
  • Benchmark early: Don’t wait until integration to test performance
  • Start with a clear understanding of limitations

We hope this breakdown helps others navigate similar problems. If you’re building something similar, we’d love to hear what worked for you!