<!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='&lt;script src="https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.js"&gt;&lt;/script&gt;'>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 &amp; 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 &amp; 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 &amp; 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>
    // Preserve links to demos that used to live on the home page.
    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' })
    }

    // One install line, npm or CDN.
    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)
    })

    // site.js fills the star count; the download count is the home page's own.
    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>