How to Fix the dom-to-image toPng Undefined Error in Ionic React
Fix dom-to-image toPng undefined in Ionic React with the right import, browser lifecycle checks, CORS guidance, and a working capture component.
Use a namespace import and call toPng only after the target element is mounted in a browser. In Ionic React, the most reliable starting point is:
import * as domtoimage from 'dom-to-image';
const node = document.getElementById('capture');
if (!node) throw new Error('Capture target not mounted');
const dataUrl = await domtoimage.toPng(node);
If domtoimage.toPng is undefined, first inspect the module export shape. If it is a function but the promise rejects, you have a rendering problem such as CORS, missing fonts, an external stylesheet, or an element that is not ready yet. Those are separate problems and need different fixes.
Why toPng is undefined
The dom-to-image API exposes top-level functions that accept a DOM node and return promises containing data URLs. An undefined method usually means your import does not match the package export that your bundler resolved.
Common causes include:
- Using a default import when the package exposes a namespace or CommonJS export.
- Having duplicate, aliased, or stale lockfile entries for
dom-to-image. - Importing a different package or fork than the one you expected.
- Calling capture during server rendering or before Ionic has mounted the page.
The original npm package is version 2.6.0 and has not been published for about nine years. That age makes lockfile and dependency-resolution mistakes worth checking before changing application code.
Fix the import in Ionic React
Recommended ES module import
import * as domtoimage from 'dom-to-image';
Then call the method through the namespace:
const dataUrl = await domtoimage.toPng(node);
Check the package and lockfile
- Install the dependency explicitly:
npm install dom-to-image
- Confirm that
package.jsonand your lockfile resolve the intended package. - Look for duplicate versions with your package manager’s dependency tree command.
- Restart the Ionic development server after changing imports or dependencies.
npm ls dom-to-image
CommonJS projects
In a CommonJS-compatible toolchain, the package README documents:
const domtoimage = require('dom-to-image');
const dataUrl = await domtoimage.toPng(node);
Do not combine a default import and namespace access until you have inspected the actual value. Temporary diagnostics can reveal the shape:
import * as imported from 'dom-to-image';
console.log(imported);
console.log(typeof imported.toPng);
console.log('default export:', imported.default);
If imported.toPng is a function, keep the namespace form. If only imported.default.toPng exists in your toolchain, use that exact shape consistently and document it for the project.
Run capture after Ionic mounts the element
Capture from an event handler or an effect that runs after render. Never call toPng at module scope, during server rendering, or before the first render has produced the target node.
import { useRef, useState } from 'react';
import * as domtoimage from 'dom-to-image';
export function CaptureCard() {
const cardRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
async function savePng() {
setError(null);
const node = cardRef.current;
if (typeof window === 'undefined') {
setError('Capture is available only in a browser.');
return;
}
if (!node) {
setError('Capture target is not mounted yet.');
return;
}
try {
const dataUrl = await domtoimage.toPng(node);
const link = document.createElement('a');
link.download = 'card.png';
link.href = dataUrl;
link.click();
} catch (cause) {
console.error('DOM capture failed', cause);
setError('The element could not be rendered. Check images, fonts, and CORS.');
}
}
return (
<>
<div ref={cardRef}>Capture me</div>
<button type="button" onClick={savePng}>Save PNG</button>
{error && <p role="alert">{error}</p>}
</>
);
}
A ref is safer than querying an ID because it follows the component instance. If you must use an ID, check for null immediately before capture.
Wait for images, fonts, and Ionic content
A mounted node can still be visually incomplete. Wait for resources that affect the result:
async function waitForResources(root: HTMLElement) {
const images = Array.from(root.querySelectorAll('img'));
await Promise.all(images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise<void>((resolve) => {
img.addEventListener('load', () => resolve(), { once: true });
img.addEventListener('error', () => resolve(), { once: true });
});
}));
if ('fonts' in document) {
await document.fonts.ready;
}
}
async function capture(node: HTMLElement) {
await waitForResources(node);
return domtoimage.toPng(node);
}
For Ionic pages that animate into view, wait until the transition has completed or capture a stable, visible container. Hidden nodes, zero-sized containers, and elements affected by an unfinished CSS transition can produce blank or partial images.
Separate undefined methods from render rejections
Use this decision sequence:
- Check the type:
typeof domtoimage.toPng === 'function'. - If false: fix the import or dependency resolution.
- If true but the promise rejects: inspect resource loading, CORS, fonts, stylesheets, and the target node.
- If it resolves but looks wrong: compare computed styles, dimensions, and loaded assets.
Cross-origin images can fail when the browser cannot fetch them with suitable CORS headers. External stylesheets and fonts can have similar access or loading problems. The original library can throw when an image fails; provide a placeholder or replace inaccessible assets according to the library’s documented options.
Useful capture options and output handling
The top-level API accepts a DOM node and rendering options. Keep options focused while diagnosing:
const dataUrl = await domtoimage.toPng(node, {
cacheBust: true,
bgcolor: '#ffffff',
imagePlaceholder: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/%3E'
});
Option names and support can vary by package version. Confirm the options supported by the exact package resolved in your lockfile. For large cards, prefer toBlob when available and create an object URL instead of keeping a large base64 string in memory:
const blob = await domtoimage.toBlob(node);
const url = URL.createObjectURL(blob);
try {
const link = document.createElement('a');
link.download = 'card.png';
link.href = url;
link.click();
} finally {
URL.revokeObjectURL(url);
}
Consider dom-to-image-more for difficult pages
dom-to-image-more documents the same domtoimage.toPng(node) workflow and adds more explicit handling for resource interception, image errors, fonts, stylesheets, and browser-only execution. Treat migration as a compatibility test:
- Update the import to the fork’s documented form.
- Run capture in a real browser and in the Ionic WebView target.
- Check web fonts, SVG, external images, and stylesheets.
- Verify that the output dimensions and colors match your existing export.
- Keep a fallback for assets that cannot be fetched with CORS.
A maintained fork does not remove browser security restrictions. It gives you clearer controls and diagnostics when those restrictions are the actual failure.
Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
toPng is undefined |
Wrong import shape | Use import * as domtoimage, or match the CommonJS/default export actually resolved. |
| Cannot read properties of undefined | Capture called before render | Use a ref and invoke from a mounted event handler. |
| Works in browser, fails during build | SSR or module-scope DOM access | Guard with typeof window !== 'undefined' and load or call the library client-side. |
| Blank output | Zero-size or hidden node, unfinished transition, or unloaded assets | Capture a visible node after layout and wait for images and fonts. |
| Missing external images | CORS or failed image request | Serve assets with appropriate CORS headers, proxy them, or use an image placeholder. |
| Missing font or stylesheet | Resource not loaded or inaccessible | Await document.fonts.ready, inspect network errors, and inline or host accessible CSS where necessary. |
| Different output in WebView | WebView engine, viewport, or font differences | Test on the target device, set explicit dimensions, and provide fallbacks. |
| Memory pressure on large captures | Large canvas and base64 data URL | Reduce dimensions, capture smaller regions, or use a Blob/object URL. |
| Changes are ignored | Cached resources | Use cache-busting options where supported and verify the resolved asset URL. |
Performance and reliability
- Capture only the required element when a full page is unnecessary.
- Keep the DOM subtree small and avoid animations during capture.
- Wait for resources once, then reuse the result when multiple exports have identical content.
- Large retina-scale canvases consume considerably more memory; choose dimensions deliberately.
- Handle the returned promise and show a retry path rather than assuming every browser resource is available.
- Log the resolved package version and import shape while diagnosing build-specific failures.
Client-side DOM capture has no API fee, but it consumes the user’s CPU and memory and inherits browser CORS and rendering behavior. A server-side screenshot service shifts those concerns away from the Ionic device.
Or skip the browser setup
For a URL screenshot, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients use screenshot, page-info, and PDF tools. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account.
FAQ
Should I use import domtoimage from 'dom-to-image'?
Use the namespace import first. A default import works only when your bundler exposes the package that way.
Can this run in an Ionic server-rendered app?
The renderer needs a browser DOM. Guard browser-only code and call it after client hydration and mount.
Does fixing the import solve CORS errors?
No. An undefined method is an import problem. CORS, fonts, and failed images are rendering-resource problems.
When should I choose a server screenshot API?
Use one when you need repeatable URL captures outside the user’s browser, want to avoid WebView differences, or need automated PDF and image jobs.
Can I capture a component without an element ID?
Yes. A React ref is usually the safest way to pass the mounted element to toPng.


