<!DOCTYPE html>
<html lang="en">
<head><script type="importmap">{"imports": {"@zumer/snapdom": "https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs", "@zumer/snapdom/plugins": "https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs"}}</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SnapDOM: Browser Capture Engine</title>
<meta name="description" content="Capture web interfaces as reusable images and canvas. Plugins add HTML, context for agents, PDF, GIF and video. MIT licensed, with zero core dependencies.">
<meta name="keywords" content="snapdom, DOM capture, html2canvas alternative, HTML to image, SVG export, JavaScript screenshot">
<meta name="author" content="zumerlab">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://snapdom.dev/">
<link rel="help" type="text/plain" href="https://snapdom.dev/llms.txt" title="LLM summary">
<link rel="help" type="text/plain" href="https://snapdom.dev/llms-full.txt" title="LLM full reference">
<meta property="og:type" content="website">
<meta property="og:title" content="SnapDOM — browser capture engine">
<meta property="og:description" content="Capture web interfaces as reusable images and canvas. Plugins add HTML, context for agents, PDF, GIF and video. MIT licensed, with zero core dependencies.">
<meta property="og:url" content="https://snapdom.dev/">
<meta property="og:image" content="https://snapdom.dev/assets/og-card.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="SnapDOM — browser capture engine">
<meta name="twitter:description" content="Capture web interfaces as reusable images and canvas. Plugins add HTML, context for agents, PDF, GIF and video. MIT licensed, with zero core dependencies.">
<meta name="twitter:image" content="https://snapdom.dev/assets/og-card.jpg">
<link rel="icon" type="image/png" href="./assets/favicon/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/svg+xml" href="./assets/favicon/favicon.svg">
<link rel="shortcut icon" href="./assets/favicon/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="./assets/favicon/apple-touch-icon.png">
<link rel="manifest" href="./assets/favicon/site.webmanifest">
<meta name="theme-color" content="#f7f9fc">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "SnapDOM",
"alternateName": "@zumer/snapdom",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Web",
"description": "Capture web interfaces as reusable images and canvas. Plugins add HTML, context for agents, PDF, GIF and video. MIT licensed, with zero core dependencies.",
"url": "https://snapdom.dev/",
"downloadUrl": "https://www.npmjs.com/package/@zumer/snapdom",
"codeRepository": "https://github.com/zumerlab/snapdom",
"license": "https://spdx.org/licenses/MIT.html",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
}
</script>
<link rel="stylesheet" href="./shared.css">
<style>
:root {
--ink: #0f1e4d;
--muted: #59647d;
--blue: #2d5bff;
--blue-dark: #1d43d7;
--paper: #f7f9fc;
--white: #fff;
--line: rgba(15, 30, 77, .14);
--soft: #eef3ff;
--max: 1180px;
--font-code: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; color: var(--ink); background: var(--paper); font-family: Arial, Helvetica, sans-serif; -webkit-font-smoothing: antialiased; }
a { color: inherit; }
.wrap { width: min(var(--max), calc(100% - 48px)); margin: 0 auto; }
.hero { padding: 96px 0 72px; border-bottom: 1px solid var(--line); }
.hero-grid { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(360px, .95fr); gap: 72px; align-items: center; }
.hero-grid > * { min-width: 0; }
.eyebrow { margin: 0 0 20px; color: var(--blue); font-size: 12px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
h1 { max-width: 760px; margin: 0; font-size: clamp(40px, 4.8vw, 64px); line-height: 1.04; letter-spacing: -.045em; text-wrap: balance; }
.lead { max-width: 670px; margin: 28px 0 0; color: var(--muted); font-size: 20px; line-height: 1.65; }
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding: 0 22px; border: 1px solid var(--ink); text-decoration: none; font-size: 15px; font-weight: 750; }
.btn-primary { border-color: var(--blue); color: var(--white); background: var(--blue); }
.btn-primary:hover { background: var(--blue-dark); }
.btn-secondary:hover { color: var(--blue); border-color: var(--blue); }
.install { width: 100%; min-width: 0; display: flex; align-items: center; gap: 18px; padding: 13px 16px; color: #dce4ff; background: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px; }
.install span { min-width: 0; overflow-x: auto; white-space: nowrap; scrollbar-width: thin; }
.install .copy { flex: 0 0 auto; }
.proof { margin-top: 30px; display: flex; flex-wrap: wrap; gap: 18px; color: var(--muted); font-size: 14px; }
.proof strong { color: var(--ink); }
.copy { border: 0; padding: 0; color: #9eb3ff; background: transparent; cursor: pointer; font: inherit; }
@keyframes flash { from { opacity: 1; } to { opacity: 0; } }
@keyframes print-in { from { opacity: 0; translate: 0 -20px; } to { opacity: 1; translate: 0 0; } }
.stage { position: relative; height: 390px; overflow: hidden; border: 1px solid var(--line); background: linear-gradient(135deg, #f0f4ff, #fff 56%, #edf7ff); }
.stage-third { position: absolute; background: rgba(45,91,255,.09); pointer-events: none; }
.stage-third.v { top: 0; bottom: 0; width: 1px; }
.stage-third.h { left: 0; right: 0; height: 1px; }
.stage-cross { position: absolute; transform: translate(-50%,-50%); color: rgba(15,30,77,.25); font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
.stage-bracket { position: absolute; width: 25px; height: 25px; }
.stage-bracket.tl { top: 14px; left: 14px; border-top: 3px solid var(--blue); border-left: 3px solid var(--blue); }
.stage-bracket.tr { top: 14px; right: 14px; border-top: 3px solid var(--blue); border-right: 3px solid var(--blue); }
.stage-bracket.bl { bottom: 14px; left: 14px; border-bottom: 3px solid var(--blue); border-left: 3px solid var(--blue); }
.stage-bracket.br { right: 14px; bottom: 14px; border-right: 3px solid var(--blue); border-bottom: 3px solid var(--blue); }
.stage-mascot { position: absolute; left: 50%; bottom: 24px; width: min(76%, 300px); height: auto; transform: translateX(-50%); }
.stage-annot { position: absolute; color: var(--muted); font: 9px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 1.2px; text-transform: uppercase; white-space: nowrap; }
.stage-annot.vert { writing-mode: vertical-rl; }
#hero-flash { position: absolute; inset: 0; z-index: 5; pointer-events: none; background: #fff; opacity: 0; }
#hero-flash.flashing { animation: flash .55s ease-out both; }
.stage-row { min-height: 128px; display: flex; align-items: flex-start; gap: 18px; padding-top: 16px; }
.shutter-wrap { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 7px; }
.shutter { width: 56px; height: 56px; display: grid; place-items: center; padding: 0; border: 2px solid var(--ink); border-radius: 50%; background: #fff; cursor: pointer; transition: transform .12s; }
.shutter span { width: 36px; height: 36px; display: block; border-radius: 50%; background: var(--blue); }
.shutter:hover span { background: var(--blue-dark); }
.shutter:active { transform: scale(.92); }
.shutter:disabled { cursor: wait; opacity: .55; }
.shutter-label { color: var(--muted); font: 9px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 1.2px; }
.hero-tray { position: relative; flex: 1; min-width: 0; display: flex; align-items: flex-start; gap: 12px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: none; }
.hero-tray::-webkit-scrollbar { display: none; }
.print { width: 158px; min-width: 0; flex: 0 1 158px; display: flex; flex-direction: column; gap: 5px; padding: 6px 6px 7px; overflow: hidden; border: 1px solid var(--line); background: #fff; box-shadow: 0 12px 26px -16px rgba(15,30,77,.45); animation: print-in .5s cubic-bezier(.2,.9,.3,1) both; }
.print img { display: block; width: 100% !important; max-width: 100%; min-width: 0; height: auto !important; object-fit: contain; }
.print-cap { color: var(--muted); font: 8px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .5px; }
.print-plug { width: 0; min-width: 100%; margin-top: -2px; overflow-wrap: anywhere; color: var(--blue); font: 8px ui-monospace, SFMono-Regular, Menlo, monospace; line-height: 1.3; }
.print.r1 { transform: rotate(1.8deg); }
.print.r2 { transform: rotate(-2.2deg); }
.star-overlay { position: absolute; inset: 0; z-index: 4; display: flex; align-items: center; justify-content: center; }
.star-overlay::before { content: ''; position: absolute; inset: 0; background: rgba(247,249,252,.68); backdrop-filter: blur(2px); }
.star-card { position: relative; border: 1px solid var(--line); background: #fff; box-shadow: 0 14px 30px -14px rgba(15,30,77,.45); transform: rotate(-2deg); animation: print-in .5s cubic-bezier(.2,.9,.3,1) both; }
.star-card a { display: flex; flex-direction: column; align-items: center; gap: 7px; padding: 22px 34px 19px; color: var(--ink); text-decoration: none; text-align: center; font: 700 14px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 1.5px; }
.star-card a:hover { color: var(--blue); }
.star-big { color: #f5a623; font-size: 40px; line-height: 1; }
.star-close { position: absolute; top: -12px; right: -12px; width: 28px; height: 28px; display: grid; place-items: center; padding: 0; border: 1px solid var(--line); border-radius: 50%; color: var(--muted); background: #fff; cursor: pointer; }
.logos { padding: 30px 0; border-bottom: 1px solid var(--line); }
.logo-row { display: flex; align-items: center; gap: 28px; overflow: hidden; color: var(--muted); }
.logo-row > span { flex: 0 0 auto; font-size: 12px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
.logo-list { display: flex; align-items: center; justify-content: space-between; flex: 1; gap: 24px; }
.logo-list a { display: inline-flex; align-items: center; color: var(--muted); text-decoration: none; }
.logo-list img { width: 28px; height: 28px; object-fit: contain; filter: grayscale(1); opacity: .75; transition: filter .15s, opacity .15s; }
.logo-list a:hover img { filter: none; opacity: 1; }
.logo-list .logo-more { color: var(--blue); font-size: 12px; font-weight: 700; white-space: nowrap; }
.logo-list .logo-more:hover { color: var(--blue-dark); }
.section { padding: 92px 0; border-bottom: 1px solid var(--line); }
.section-heading { display: grid; grid-template-columns: .75fr 1.25fr; gap: 48px; margin-bottom: 44px; }
.section-heading h2 { margin: 0; font-size: clamp(36px, 5vw, 58px); line-height: 1; letter-spacing: -.045em; }
.section-heading p { max-width: 620px; margin: 4px 0 0; color: var(--muted); font-size: 18px; line-height: 1.65; }
.section-white { background: var(--white); }
.install-line { width: 100%; max-width: 600px; margin-top: 28px; background: var(--ink); }
.install-tabs { display: flex; border-bottom: 1px solid rgba(217,226,247,.16); }
.install-tabs button { padding: 9px 14px; border: 0; border-bottom: 2px solid transparent; background: none; color: #b9c5e4; cursor: pointer; font: 700 12px Arial, Helvetica, sans-serif; }
.install-tabs button[aria-selected="true"] { border-bottom-color: #9eb3ff; color: var(--white); }
.install-line .install { margin: 0; }
.demo-panel .stage-row { min-height: 128px; }
.tray-empty { padding-top: 22px; color: var(--muted); opacity: .7; font: 9px var(--font-code); letter-spacing: 1.2px; text-transform: uppercase; }
.output-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.output-card { display: flex; flex-direction: column; gap: 12px; padding: 24px; background: var(--white); border: 1px solid var(--line); transition: border-color .15s; }
.output-card:hover { border-color: var(--blue); }
.output-card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.output-card h3 { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 23px; line-height: 1.2; letter-spacing: -.025em; }
.output-card-formats { color: var(--muted); font: 400 12.5px/1.5 var(--font-code); }
.output-card p { flex: 1; margin: 0; color: var(--muted); font-size: 15px; line-height: 1.6; }
.output-card-links { display: flex; flex-wrap: wrap; align-items: center; gap: 18px; padding-top: 4px; }
.output-card-links a { color: var(--blue); font-size: 14px; font-weight: 700; text-underline-offset: 4px; }
.output-card:hover .output-card-links a:first-child { text-decoration-thickness: 2px; }
.output-card-links a.is-sub { color: var(--muted); font-size: 13px; font-weight: 400; }
.index-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.showcase-callout { display: grid; grid-template-columns: 1fr 1fr; gap: 50px; align-items: center; }
.showcase-visual { position: relative; min-height: 360px; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--line); background: #fff; }
.showcase-visual img { width: 300px; max-width: 80%; position: relative; }
.showcase-copy h2 { margin: 0; font-size: clamp(38px, 5vw, 62px); line-height: 1; letter-spacing: -.045em; }
.showcase-copy p { margin: 22px 0 28px; color: var(--muted); font-size: 18px; line-height: 1.65; }
.final-cta { padding: 90px 0; text-align: center; color: var(--white); background: var(--ink); }
.btn-inverse { color: var(--white); border-color: rgba(255,255,255,.55); }
.btn-inverse:hover { border-color: var(--white); }
.final-cta h2 { max-width: 780px; margin: 0 auto; font-size: clamp(42px, 6vw, 72px); line-height: 1; letter-spacing: -.05em; }
.final-cta .actions { justify-content: center; }
@media (max-width: 1080px) {
.hero { padding-top: 64px; }
.hero-grid, .section-heading, .showcase-callout { grid-template-columns: 1fr; }
.hero .demo-panel { max-width: 620px; }
.output-grid, .index-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.hero-tray { gap: 8px; }
.print { width: 104px; min-width: 80px; flex: 0 1 104px; gap: 4px; padding: 5px; }
.print-cap, .print-plug { font-size: 7px; letter-spacing: .3px; }
.print-cap { white-space: nowrap; }
.print-plug { width: auto; min-width: 0; max-width: 100%; margin-top: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
@media (max-width: 600px) {
.wrap { width: min(100% - 28px, var(--max)); }
.hero { padding: 48px 0 54px; }
.lead { font-size: 17px; }
.stage { height: 310px; }
.stage-annot.vert { display: none; }
.stage-annot { font-size: 8px; }
.hero-tray { gap: 7px; }
.print { width: 78px; min-width: 64px; flex: 0 1 78px; gap: 3px; padding: 4px; }
.print-cap, .print-plug { font-size: 6.5px; letter-spacing: .2px; }
.logo-row { align-items: flex-start; flex-direction: column; gap: 14px; }
.logo-list { width: 100%; justify-content: flex-start; flex-wrap: wrap; gap: 16px 24px; }
.logo-list a:nth-child(n+5):not(.logo-more) { display: none; }
.logo-list .logo-more { margin-left: auto; }
.section { padding: 64px 0; }
.showcase-visual { min-height: 280px; }
.output-grid, .index-grid-4 { grid-template-columns: minmax(0, 1fr); }
}
</style>
<link rel="stylesheet" href="./navigation.css">
<link rel="stylesheet" href="./panes.css">
<script defer src="./site.js"></script>
<link rel="stylesheet" href="code.css"><script defer data-manual src="assets/prism/prism.min.js"></script><script defer src="code-blocks.js"></script></head>
<body>
<header class="site-head"><div class="site-head-inner"><a class="site-shell-brand" href="./" aria-label="SnapDOM home"><span class="site-shell-mark"></span><span>SnapDOM</span></a><nav class="site-nav" aria-label="Main navigation"><div class="site-nav-group"><button type="button" class="site-nav-button" aria-expanded="false">Learn</button><div class="site-nav-panel"><a class="site-nav-item sec-capture" href="docs/"><span class="sec-dot"></span><span class="site-nav-item-label">Docs</span><span class="site-nav-item-desc">API, options and quick start</span></a><a class="site-nav-item sec-start" href="how-to/"><span class="sec-dot"></span><span class="site-nav-item-label">How-to</span><span class="site-nav-item-desc">Recipes and framework guides</span></a><a class="site-nav-item sec-ecosystem" href="compare/"><span class="sec-dot"></span><span class="site-nav-item-label">Compare</span><span class="site-nav-item-desc">SnapDOM vs html2canvas and others</span></a></div></div><div class="site-nav-group"><button type="button" class="site-nav-button" aria-expanded="false">Explore</button><div class="site-nav-panel"><a class="site-nav-item sec-labs" href="showcase/"><span class="sec-dot"></span><span class="site-nav-item-label">Showcase</span><span class="site-nav-item-desc">Live demos, plugins and labs</span></a><a class="site-nav-item sec-record" href="blog/"><span class="sec-dot"></span><span class="site-nav-item-label">Blog</span><span class="site-nav-item-desc">Notes from the team</span></a></div></div><div class="site-nav-group"><button type="button" class="site-nav-button" aria-expanded="false">Community</button><div class="site-nav-panel"><a class="site-nav-item sec-ecosystem" href="ecosystem/"><span class="sec-dot"></span><span class="site-nav-item-label">Ecosystem</span><span class="site-nav-item-desc">SnapDIFF, SnapEye, SnapSurf</span></a><a class="site-nav-item sec-context" href="made-with/"><span class="sec-dot"></span><span class="site-nav-item-label">Made with</span><span class="site-nav-item-desc">Projects using SnapDOM</span></a></div></div></nav><button type="button" class="site-menu-button" aria-expanded="false">Menu</button><a class="site-shell-github" aria-label="SnapDOM on GitHub" href="https://github.com/zumerlab/snapdom" target="_blank" rel="noopener"><span>GitHub</span><span class="site-shell-stars">★ <span data-star-count>8K</span></span></a></div></header>
<main>
<section class="hero">
<div class="wrap hero-grid">
<div>
<p class="eyebrow">Browser capture engine · zero dependencies</p>
<h1>HTML to images, documents and context.</h1>
<p class="lead">SnapDOM captures rendered HTML in your browser. Save it as an image, or add a plugin for PDF, HTML, recordings or agent context.</p>
<div class="actions">
<a class="btn btn-primary" href="./docs/">Get started</a>
<a class="btn btn-secondary" href="#outputs">Choose your output</a>
</div>
<div class="install-line">
<div class="install-tabs" role="tablist" aria-label="Install with">
<button type="button" role="tab" aria-selected="true" data-install="npm i @zumer/snapdom@latest">npm</button>
<button type="button" role="tab" aria-selected="false" data-install='<script src="https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.js"></script>'>CDN</button>
</div>
<div class="install"><span id="install-command">npm i @zumer/snapdom@latest</span><button class="copy" type="button" id="install-copy">copy</button></div>
</div>
<div class="proof" aria-label="Project stats">
<span id="stable-documentation" hidden></span>
<span><strong id="download-count">1m+</strong> downloads/month</span>
<span><strong>MIT</strong> licensed</span>
</div>
</div>
<section class="demo-panel sec-capture" aria-label="Live demo">
<div class="demo-panel-body">
<div class="stage" id="hero-stage">
<div class="stage-third v" style="left:33.33%"></div>
<div class="stage-third v" style="left:66.66%"></div>
<div class="stage-third h" style="top:33.33%"></div>
<div class="stage-third h" style="top:66.66%"></div>
<span class="stage-cross" style="left:33.33%;top:33.33%">+</span>
<span class="stage-cross" style="left:66.66%;top:66.66%">+</span>
<div class="stage-bracket tl"></div><div class="stage-bracket tr"></div><div class="stage-bracket bl"></div><div class="stage-bracket br"></div>
<img class="stage-mascot mascot-art" src="./assets/mascots/mr-snapdom-guides.webp" alt="Mr. SnapDOM guiding the way with a map" width="1254" height="1254" decoding="async">
<span class="stage-annot" style="top:20px;left:52px">Subject: Mr. SnapDOM</span>
<span class="stage-annot" style="bottom:20px;right:52px">AF · locked</span>
<span class="stage-annot vert" style="top:50%;right:20px;transform:translateY(-50%)">Live DOM: not a screenshot</span>
<span class="stage-annot" style="bottom:20px;left:52px">Fig. 01: moments before capture</span>
<div id="hero-flash"></div>
</div>
<div class="stage-row">
<div class="shutter-wrap"><button class="shutter" id="hero-shutter" type="button" aria-label="Capture Mr. SnapDOM"><span></span></button><span class="shutter-label">CAPTURE</span></div>
<div class="hero-tray" id="hero-tray" aria-live="polite"><span class="tray-empty">Prints land here</span></div>
</div>
</div>
<footer class="demo-panel-status" id="hero-status" role="status">Press the shutter. This stage is captured for real.</footer>
</section>
</div>
</section>
<section class="logos" aria-label="Used in production by">
<div class="wrap logo-row">
<span>Used in production by</span>
<div class="logo-list">
<a href="https://github.com/lobehub/lobehub" target="_blank" rel="noopener" aria-label="LobeHub repository"><img src="./assets/made-with/lobehub.png" alt="LobeHub" title="LobeHub"></a>
<a href="https://github.com/huggingface/chat-ui" target="_blank" rel="noopener" aria-label="Hugging Face Chat UI repository"><img src="./assets/made-with/huggingface.png" alt="Hugging Face" title="Hugging Face Chat UI"></a>
<a href="https://github.com/Tencent/tmagic-editor" target="_blank" rel="noopener" aria-label="Tencent TMagic Editor repository"><img src="./assets/made-with/Tencent.png" alt="Tencent" title="Tencent TMagic Editor"></a>
<a href="https://github.com/seek-oss/playroom" target="_blank" rel="noopener" aria-label="SEEK Playroom repository"><img src="./assets/made-with/seek-oss.png" alt="SEEK" title="SEEK Playroom"></a>
<a href="https://github.com/antvis/GPT-Vis" target="_blank" rel="noopener" aria-label="AntV GPT-Vis repository"><img src="./assets/made-with/antvis.png" alt="AntV" title="AntV GPT-Vis"></a>
<a href="https://github.com/metabrainz/listenbrainz-server" target="_blank" rel="noopener" aria-label="MetaBrainz ListenBrainz repository"><img src="./assets/made-with/metabrainz.png" alt="MetaBrainz" title="MetaBrainz ListenBrainz"></a>
<a class="logo-more" href="./made-with/">View more projects</a>
</div>
</div>
</section>
<section class="section" id="outputs">
<div class="wrap">
<div class="section-heading">
<h2>One interface.<br>Several ways to use it.</h2>
<p>Start with the result you need. The tag tells you whether it ships with the core or needs a plugin.</p>
</div>
<div class="output-grid">
<article class="output-card sec-capture">
<div class="output-card-head"><h3><span class="sec-dot"></span>Images & canvas</h3><span class="tag tag-core">Core</span></div>
<span class="output-card-formats">PNG · JPEG · WebP · canvas · Blob</span>
<p>Save a chart, dashboard or component as an image. Use canvas output as a WebGL texture.</p>
<div class="output-card-links"><a href="./showcase/#plugins">Start with images</a></div>
</article>
<article class="output-card sec-export">
<div class="output-card-head"><h3><span class="sec-dot"></span>PDF documents</h3><span class="tag tag-plugin">Plugin</span></div>
<span class="output-card-formats">PDF with text, links & pages</span>
<p>Turn an HTML report or invoice into a searchable document with page breaks and bookmarks.</p>
<div class="output-card-links"><a href="./pro/pdf/">Try PDF</a></div>
</article>
<article class="output-card sec-export">
<div class="output-card-head"><h3><span class="sec-dot"></span>HTML snapshots</h3><span class="tag tag-plugin">Plugin</span></div>
<span class="output-card-formats">A self-contained HTML file</span>
<p>Save the rendered markup with its styles and resources. Reopen it without rebuilding your app.</p>
<div class="output-card-links"><a href="./showcase/#plugins">Try HTML</a></div>
</article>
<article class="output-card sec-record">
<div class="output-card-head"><h3><span class="sec-dot"></span>GIF & video</h3><span class="tag tag-plugin">Plugins</span></div>
<span class="output-card-formats">GIF · MP4 or WebM</span>
<p>Record the live element over time for short animations, demos and review clips.</p>
<div class="output-card-links"><a href="./showcase/#plugins">Try GIF</a><a class="is-sub" href="./showcase/#video-export-demo">Or record video</a></div>
</article>
<article class="output-card sec-context">
<div class="output-card-head"><h3><span class="sec-dot"></span>Context for agents</h3><span class="tag tag-plugin">Plugins</span></div>
<span class="output-card-formats">Text · JSON · annotated image</span>
<p>Give an agent the captured text, form state and locations of interactive elements.</p>
<div class="output-card-links"><a href="./showcase/#plugins">Try an element map</a><a class="is-sub" href="./showcase/#plugins">Or get a text / JSON tree</a></div>
</article>
<article class="output-card sec-export">
<div class="output-card-head"><h3><span class="sec-dot"></span>Vector artwork</h3><span class="tag tag-plugin">Plugin</span></div>
<span class="output-card-formats">SVG shapes · Figma</span>
<p>Turn captured text and supported shapes into editable SVG artwork or Figma content.</p>
<div class="output-card-links"><a href="./pro/vector/">Explore Vector</a></div>
</article>
</div>
</div>
</section>
<section class="section section-white">
<div class="wrap">
<div class="section-heading">
<h2>How the pieces fit.</h2>
<p>SnapDOM captures the interface. Plugins prepare or export that capture. Separate tools compare results or operate a browser.</p>
</div>
<div class="pipeline"><div class="pipeline-box"><span class="pipeline-eyebrow"><span class="sec-dot" style="background:rgba(15,30,77,.3)"></span>Your source</span><strong>A rendered HTML interface</strong><p>An element, component, report or dashboard with its CSS and resources.</p></div><span class="pipeline-link" aria-hidden="true"><i></i><i></i></span><div class="pipeline-box is-engine sec-capture"><span class="pipeline-eyebrow"><span class="sec-dot"></span>SnapDOM core</span><strong>Capture its current state</strong><p>Styles, fonts, images, layout and control values.</p></div><span class="pipeline-link" aria-hidden="true"><i></i><i></i></span><div class="pipeline-box"><span class="pipeline-eyebrow">Choose your result</span><ul><li class="sec-capture"><span class="sec-dot"></span>Image / canvas<small>Core</small></li><li class="sec-export"><span class="sec-dot"></span>PDF document<small>Plugin</small></li><li class="sec-export"><span class="sec-dot"></span>HTML snapshot<small>Plugin</small></li><li class="sec-record"><span class="sec-dot"></span>GIF / video<small>Plugins</small></li><li class="sec-context"><span class="sec-dot"></span>Text / element map<small>Plugins</small></li></ul></div></div>
</div>
</section>
<section class="section">
<div class="wrap">
<div class="section-heading">
<h2>Documentation and guides.</h2>
<p>Each area has its own colour. The same colour marks it in the sidebar, on its live demo and in its page titles.</p>
</div>
<div class="index-grid index-grid-4"><a class="sec-start" href="./docs/"><span class="sec-dot"></span><span class="index-grid-label">Getting started</span><span class="index-grid-meta">docs</span></a><a class="sec-capture" href="./docs/api/"><span class="sec-dot"></span><span class="index-grid-label">Capture API</span><span class="index-grid-meta">api</span></a><a class="sec-options" href="./docs/options/"><span class="sec-dot"></span><span class="index-grid-label">Options</span><span class="index-grid-meta">settings</span></a><a class="sec-prepare" href="./showcase/#plugins"><span class="sec-dot"></span><span class="index-grid-label">Preparation</span><span class="index-grid-meta">5 plugins</span></a><a class="sec-export" href="./showcase/#plugins"><span class="sec-dot"></span><span class="index-grid-label">Export</span><span class="index-grid-meta">plugins</span></a><a class="sec-record" href="./showcase/#plugins"><span class="sec-dot"></span><span class="index-grid-label">Recording</span><span class="index-grid-meta">2 plugins</span></a><a class="sec-context" href="./showcase/#plugins"><span class="sec-dot"></span><span class="index-grid-label">Agent context</span><span class="index-grid-meta">2 plugins</span></a><a class="sec-ecosystem" href="./showcase/#plugins"><span class="sec-dot"></span><span class="index-grid-label">All plugins</span><span class="index-grid-meta">12</span></a><a class="sec-capture" href="./showcase/#plugins"><span class="sec-dot"></span><span class="index-grid-label">Plugin demos</span><span class="index-grid-meta">live</span></a><a class="sec-labs" href="./showcase/"><span class="sec-dot"></span><span class="index-grid-label">Showcase</span><span class="index-grid-meta">live demos</span></a><a class="sec-start" href="./how-to/"><span class="sec-dot"></span><span class="index-grid-label">How-to recipes</span><span class="index-grid-meta">17 recipes</span></a><a class="sec-ecosystem" href="./compare/"><span class="sec-dot"></span><span class="index-grid-label">Compare</span><span class="index-grid-meta">7 comparisons</span></a><a class="sec-options" href="./how-to/#frameworks"><span class="sec-dot"></span><span class="index-grid-label">Guides</span><span class="index-grid-meta">6 frameworks</span></a><a class="sec-labs" href="./showcase/#labs"><span class="sec-dot"></span><span class="index-grid-label">Labs</span><span class="index-grid-meta">7 experiments</span></a><a class="sec-record" href="./blog/"><span class="sec-dot"></span><span class="index-grid-label">Blog</span><span class="index-grid-meta">4 posts</span></a><a class="sec-context" href="./made-with/"><span class="sec-dot"></span><span class="index-grid-label">Made with</span><span class="index-grid-meta">projects</span></a></div>
</div>
</section>
<section class="section">
<div class="wrap showcase-callout">
<div class="showcase-visual"><img class="mascot-art" src="./assets/mascots/mr-snapdom-showcase.webp" alt="Mr. SnapDOM presenting an image frame" width="1254" height="1254" loading="lazy" decoding="async"></div>
<div class="showcase-copy">
<p class="eyebrow">Live demos</p>
<h2>Run the complete demo suite.</h2>
<p>Run SnapDOM against canvas, video, fonts, masks, transforms, Shadow DOM, iframes and plugin exporters. Every example uses the real library.</p>
<a class="btn btn-primary" href="./showcase/">Open the showcase</a>
</div>
</div>
</section>
<section class="final-cta">
<div class="wrap">
<h2>Install SnapDOM and capture your first element.</h2>
<div class="actions">
<a class="btn btn-primary" href="./docs/">Get started</a>
<a class="btn btn-inverse" href="https://github.com/zumerlab/snapdom" target="_blank" rel="noopener">★ Star on GitHub</a>
</div>
</div>
</section>
</main>
<footer class="site-foot"><nav class="site-foot-directory" aria-label="Site directory"><div><h2>Learn</h2><a href="docs/">Documentation</a><a href="docs/#quick-start">Quick start</a><a href="how-to/">How-to recipes</a><a href="how-to/#frameworks">Framework guides</a><a href="compare/">Compare</a></div><div><h2>Explore</h2><a href="showcase/">Showcase</a><a href="showcase/#plugins">Plugins</a><a href="docs/plugins/#build">Writing plugins</a><a href="showcase/#labs">Labs</a><a href="blog/">Blog</a></div><div><h2>Community</h2><a href="ecosystem/">Ecosystem</a><a href="made-with/">Made with</a><a href="ecosystem/#community-plugins">Community plugins</a><a href="https://github.com/zumerlab/snapdom">GitHub</a><a href="https://www.npmjs.com/package/@zumer/snapdom">npm</a><a href="https://github.com/sponsors/tinchox5">Sponsors</a></div></nav><div class="site-foot-legal"><p>MIT © <a href="https://zumerlab.com/">Zumerlab</a></p></div></footer>
<script>
const showcaseAnchors = ['demo-index', 'benchmark', 'basic', 'transition', 'opts-demo', 'orbit', 'html-export-demo', 'gif-export-demo', 'video-export-demo', 'video', 'fonts-demo', 'shadow', 'canvas', 'export', 'pseudo', 'clip', 'blend', 'iframe', 'forms', 'mask', 'cors', 'fullbody']
if (showcaseAnchors.includes(location.hash.slice(1))) {
location.replace('./showcase/' + location.hash)
}
if (location.hostname === 'snapdom.dev') {
const umami = document.createElement('script')
umami.defer = true
umami.src = 'https://cloud.umami.is/script.js'
umami.dataset.websiteId = '7ec718e8-f0c5-4abb-8f4f-144f57f61937'
document.head.appendChild(umami)
const analytics = document.createElement('script')
analytics.async = true
analytics.src = 'https://www.googletagmanager.com/gtag/js?id=G-4G88ZHG7S1'
document.head.appendChild(analytics)
window.dataLayer = window.dataLayer || []
function gtag(){ dataLayer.push(arguments) }
gtag('js', new Date())
gtag('config', 'G-4G88ZHG7S1', { transport_type: 'beacon' })
}
const installCommand = document.getElementById('install-command')
const installCopy = document.getElementById('install-copy')
document.querySelectorAll('[data-install]').forEach(tab => tab.addEventListener('click', () => {
document.querySelectorAll('[data-install]').forEach(t => t.setAttribute('aria-selected', String(t === tab)))
installCommand.textContent = tab.dataset.install
}))
installCopy.addEventListener('click', async () => {
await navigator.clipboard.writeText(installCommand.textContent)
installCopy.textContent = 'copied'
setTimeout(() => { installCopy.textContent = 'copy' }, 1200)
})
fetch('https://api.npmjs.org/downloads/point/last-month/%40zumer%2Fsnapdom')
.then(response => response.json())
.then(npm => {
if (npm.downloads) document.getElementById('download-count').textContent = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(npm.downloads)
})
.catch(() => {})
</script>
<script type="importmap">
{ "imports": { "@zumer/snapdom": "https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs" } }
</script>
<script type="module">
import { snapdom } from 'https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs'
const pluginRolls = [
{ mod: 'filter', fn: 'filter', opts: { preset: 'vintage' }, label: "filter({ preset: 'vintage' })" },
{ mod: 'filter', fn: 'filter', opts: { preset: 'dramatic' }, label: "filter({ preset: 'dramatic' })" },
{ mod: 'filter', fn: 'filter', opts: { preset: 'grayscale' }, label: "filter({ preset: 'grayscale' })" },
{ mod: 'filter', fn: 'filter', opts: { preset: 'invert' }, label: "filter({ preset: 'invert' })" },
{ mod: 'color-tint', fn: 'colorTint', opts: { color: '#2D5BFF', opacity: .9 }, label: "colorTint({ color: '#2D5BFF' })" },
{ mod: 'color-tint', fn: 'colorTint', opts: { color: '#FF6B35', opacity: .9 }, label: "colorTint({ color: '#FF6B35' })" },
{ mod: 'timestamp-overlay', fn: 'timestampOverlay', opts: { format: 'time', position: 'bottom-right', fontSize: 26 }, label: "timestampOverlay({ format: 'time' })" }
]
let captureBusy = false
let captureCount = 0
let lastPlugin = -1
let starShown = false
function nextPlugin() {
if (captureCount++ === 0) return null
let index = lastPlugin
while (index === lastPlugin) index = Math.floor(Math.random() * pluginRolls.length)
lastPlugin = index
return pluginRolls[index]
}
async function captureHero() {
if (captureBusy || document.querySelector('.star-overlay')) return
captureBusy = true
const button = document.getElementById('hero-shutter')
const status = document.getElementById('hero-status')
const stage = document.getElementById('hero-stage')
const tray = document.getElementById('hero-tray')
const flash = document.getElementById('hero-flash')
button.disabled = true
try {
flash.classList.remove('flashing')
void flash.offsetWidth
flash.classList.add('flashing')
const roll = nextPlugin()
let plugins = []
let label = 'snapdom.toPng()'
if (roll) {
try {
const plugin = await import(`https://esm.sh/@zumer/snapdom-plugins@latest/${roll.mod}?external=@zumer/snapdom`)
plugins = [plugin[roll.fn](roll.opts)]
label = roll.label
} catch (error) {
console.warn('Hero plugin failed; using a plain capture.', error)
}
}
const started = performance.now()
const image = await snapdom.toPng(stage, { scale: .32, embedFonts: false, cache: 'auto', exclude: ['#hero-flash'], plugins })
const print = document.createElement('div')
print.className = `print r${(tray.childElementCount % 2) + 1}`
image.alt = 'Mr. SnapDOM capture'
image.draggable = false
const caption = document.createElement('div')
caption.className = 'print-cap'
const ms = Math.max(1, Math.round(performance.now() - started))
caption.textContent = `hero.png · ${ms} ms`
status.textContent = `${label.replace('snapdom.', '')} · ${image.naturalWidth} × ${image.naturalHeight} px · ${ms} ms`
tray.querySelector('.tray-empty')?.remove()
const pluginName = document.createElement('div')
pluginName.className = 'print-plug'
pluginName.textContent = label
pluginName.title = label
print.append(image, caption, pluginName)
tray.prepend(print)
while (tray.childElementCount > 2) tray.lastElementChild.remove()
if (captureCount >= 7 && !starShown) {
starShown = true
const overlay = document.createElement('div')
overlay.className = 'star-overlay'
overlay.innerHTML = '<div class="star-card"><a href="https://github.com/zumerlab/snapdom" target="_blank" rel="noopener"><span class="star-big">★</span>ENJOYING IT?<br>STAR THE REPO</a><button class="star-close" type="button" aria-label="Close">×</button></div>'
const close = () => { overlay.remove(); button.disabled = false }
overlay.querySelector('.star-close').addEventListener('click', close)
overlay.querySelector('a').addEventListener('click', close)
tray.append(overlay)
}
} catch (error) {
status.textContent = 'Error: ' + (error?.message || error)
} finally {
captureBusy = false
button.disabled = !!document.querySelector('.star-overlay')
}
}
document.getElementById('hero-shutter').addEventListener('click', captureHero)
</script>
</body>
</html>