Tearline

Try it

Tearline

Playground

Docs

API

FAQ

Try it

Tearline

Playground

Docs

API

FAQ

Try it

Decision

Four ways to make
a share image.

the short answer

A share image is either drawn in the visitor's browser or drawn on a server, and that fork decides everything else. A custom element for share images belongs to the first branch: the tag renders the card in the page, and the same tag exports it as a PNG when someone clicks. The second branch — satori, or @vercel/og wrapping it — draws the image without a browser, which is the only way an Open Graph card can work, because the crawler that fetches it will never run your JavaScript.

So the question is not which library is better. It is whether the person the image is for is present. A user staring at their listening history and wanting a PNG of it is present, and the browser already has the receipt laid out. A social crawler asking for a preview card is not present, and there is no layout to reuse.

the size difference, and why

The client-side packages are two orders of magnitude smaller than the server ones — 186 KB and 315 KB unpacked against 5.43 MB and 6.89 MB. That gap is structural rather than a matter of care. Rendering in a browser means borrowing the layout engine that is already in the room, which is what the <foreignObject> technique actually does. Rendering on a server means there is no layout engine to borrow and one has to be shipped.

route · version, deps, licence, size, published · when it wins

a custom elementone script tag · 0 deps · MITThe element renders the thing AND exports it. Right when the user is looking at the artefact and wants a PNG of it on click. Tearline is 17,991 bytes served, HTTP 200 on 13 Aug 2026.
modern-screenshot4.7.0 · 0 deps · MIT · 186 KB · 16 Apr 2026A rasteriser you point at any node you already have on screen. Right when the share image is an existing part of the page rather than a purpose-built card.
html-to-image1.11.13 · 0 deps · MIT · 315 KB · 14 Feb 2025Same job, longer-standing. Also zero declared runtime dependencies. Both of these leave the markup, the styling and the click handler to you.
satori0.29.0 · 11 deps · MPL-2.0 · 5.43 MB · 23 Jul 2026Renders the image on a server, so no browser is involved and no user has to be present. Right for an Open Graph card, which a crawler has to fetch without running your app.
@vercel/og1.0.1 · 2 deps · MPL-2.0 · 6.89 MB · 8 Aug 2026The same server route packaged for a framework route handler. Its two declared runtime dependencies are satori and @resvg/resvg-wasm, so the licence is MPL-2.0 here too, not MIT.

Registry facts, not a review: read from registry.npmjs.org on 9 August 2026. @vercel/og left 0.x four days after this page first ran the table — 1.0.1 published 8 August 2026, two runtime dependencies still, and marginally smaller unpacked at 6.89 MB. One licence detail worth catching before it reaches a legal review — the two client-side rasterisers are MIT, and both server-side packages are MPL-2.0. @vercel/og declares exactly two runtime dependencies, satori and @resvg/resvg-wasm, which is why the licence carries through. Nothing here is a claim about how any of these four work internally: their metadata was fetched, their source was not.

Why a tag

The tag can ship
before the script does.

upgrade, quoted

The strongest argument for making a share-image widget a custom element rather than a framework component is a guarantee written into the HTML Standard, and it has a name: upgrade. An element whose definition has not loaded yet is not an error. The standard walks through a script marked async placed after the tag: while the script is loading, “the img-viewer element will be treated as an undefined element, similar to a span”, and once it loads, “the existing img-viewer element on the page will be upgraded, applying the custom element’s definition”.

For a share image that is the whole game. The card can be in the server-rendered HTML, in a CMS field, in a Markdown file, in an email template someone else owns — and the script that turns it into an exportable receipt can arrive late, out of order, or from a CDN. Nothing has to co-ordinate. A framework component cannot make that promise, because the markup does not exist until the framework has booted.

the one caveat, also quoted

The standard is explicit that this has a boundary: “upgrades only apply to elements in the document tree” — formally, elements that are connected — and “an element that is not inserted into a document will stay un-upgraded”. So a share card built in memory and held there has no methods on it. Insert it, then export it.

what the boundary also buys

Two more things follow from the element boundary rather than from any particular implementation. The first is that the content stays real content. Text wrapped in a custom element sits in the light DOM, so it is selectable, searchable, translatable and read by a screen reader in document order — which a canvas-drawn or server-drawn card cannot offer, because a PNG has no text in it at all.

The second is that the styling is negotiable. Rules inside a shadow tree lose to ordinary rules on the host page, so a component's own look is a default rather than a fight — the mechanism, and the exact cascade wording, are in the receipt-UI write-up.

and what it does not buy

Being a custom element does nothing about the export sandbox. Any client-side route — element or library — hits the same two failures: a remote image or webfont will not load inside the serialised SVG, and cross-origin data drawn onto the canvas taints it so the pixels cannot be read back. Both are written up with their sources on the DOM-to-PNG page.

Contract

What the element
has to implement.

a valid custom element name — all five must hold

a valid element local nameThe base requirement. Per the standard, this “ensures the custom element can be created with createElement()”.
starts with a lowercase letter“name’s 0th code point is an ASCII lower alpha” — which “ensures the HTML parser will treat the name as a tag name instead of as text”.
no capitals anywhere“name does not contain any ASCII upper alphas”, so that a user agent “can always treat HTML elements ASCII-case-insensitively”.
contains a hyphen“name contains a U+002D (-)”, for namespacing and forward compatibility — no hyphenated local names will be added to HTML, SVG or MathML going forward.
not one of eight reserved namesannotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph. All hyphenated names that already exist in SVG or MathML.

Quoted from the HTML Standard's custom-elements section, fetched 5 August 2026. The hyphen requirement is the one people trip over: sharecard is not a legal custom element name and share-card is. Get it wrong and the registration throws rather than failing quietly.

share-card.js

share-card.js
1
2
3
4
5
6
7
8
9
10
11
12
13
// the whole contract for a share-image element
class ShareCard extends HTMLElement {
static observedAttributes = ['width', 'seed'];
attributeChangedCallback() { this.render(); }
connectedCallback() { this.render(); }
// the part that makes it a SHARE-image element
async toBlob({ scale = 2 } = {}) { /* ... */ }
async download(name) { /* ... */ }
}
customElements.define('share-card', ShareCard);

Beyond the name, a share-image element needs three things. A static observedAttributes array, so that attributeChangedCallback() fires when the card's inputs change; a connectedCallback(), because that is the point at which the element is in the document and can measure itself; and an export method that returns a Blob rather than triggering a download, so the caller can upload it, put it on the clipboard or hand it to the Web Share API instead.

Determinism is the non-obvious requirement. If the card has any randomised element — a texture, a torn edge, a rotation — it has to be seeded, or the exported PNG will not match the card the user was looking at when they clicked. Tearline takes a seed attribute for exactly this reason.

Fit

Where this one fits.
And where it does not.

what Tearline is

Tearline is a worked example of the first row in that table, with one strong opinion: it only makes one kind of picture. Wrap markup in <tear-line> and it renders as a thermal receipt — paper texture, dashed rules, seeded torn edge, barcode — and the element exports that. One script tag, no build step, no runtime dependencies, 17,991 bytes served unminified at /tearline.js and HTTP 200 on 5 August 2026.

The full attribute and method reference is in the documentation. A worked build of the genre most people arrive looking for — a listening-history receipt, and the API cap that stops most of them shipping — is a separate write-up.

when to use something else

Three cases, stated plainly. If the share image needs to be an Open Graph card that a crawler fetches, no client-side element can do it and the server route is the answer. If the share image should look like anything other than a receipt, this component is the wrong shape and a general rasteriser pointed at your own markup is the right one. And if the requirement is a PNG of some part of the page that already exists, modern-screenshot or html-to-image is a closer fit than any purpose-built element.

Read it before you adopt it, because all of it is readable. The source is MIT at github.com/kyisaiah47/tearline, public and anonymous — an unauthenticated GET of the raw file returned HTTP 200 on 13 August 2026, byte-identical to what this origin serves. The same component is served unminified at /tearline.js, so there is no minified build hiding a second implementation. It installs as @kynth/tearline on npm, or from the script tag above — npm refuses the bare name tearline as too close to readline, so the package is scoped.

Sources

Every number here.
Fetched, not remembered.

checked 5 August 2026

registry.npmjs.orgLatest version, licence, declared runtime dependency count, publish date and unpacked size for satori, @vercel/og, modern-screenshot and html-to-image. Re-fetched 9 August 2026; @vercel/og had published 1.0.1 the day before, the other three were unchanged.
html.spec.whatwg.orgThe five requirements for a valid custom element name, and the upgrade behaviour quoted below — both from the HTML Standard's custom-elements section. Fetched 5 August 2026.
tearline.kynth.studio/tearline.jsThe served component: 17,991 bytes, unminified, HTTP 200 on 13 August 2026. It is the whole thing; there is nothing else to read.

why this is here

Package versions, licences and sizes go stale, and a page that quotes them from memory is wrong within weeks without ever looking wrong. Every figure above carries the date it was read, so you can tell at a glance how much to trust it — and so can we, because this page is on a register that gets re-checked against these same sources.

The spec quotations are from the WHATWG HTML Standard itself rather than from a summary of it, because it is a living standard and a summary is a snapshot of one. If a number here has drifted, the source is the authority, not this page.

Tearline

Wrap any HTML in one tag and it prints as a receipt.

Product

Overview

Features

Playground

FAQ

Docs

Install

API reference

Exporting a PNG

Accessibility

DOM to PNG, explained

Receipt-style UI

Spotify receipt generators

Share images, four ways

Code

Source on GitHub

@kynth/tearline on npm

Read the source

llms.txt

MIT licence

Studio

Kynth Studios

© 2026 Tearline. MIT licensed — free forever. A Kynth Studios project. The studio behind Agentwire, BreachProbe and CiteRank.

A Kynth Studios project

Tearline

Wrap any HTML in one tag and it prints as a receipt.

Product

Overview

Features

Playground

FAQ

Docs

Install

API reference

Exporting a PNG

Accessibility

DOM to PNG, explained

Receipt-style UI

Spotify receipt generators

Share images, four ways

Code

Source on GitHub

@kynth/tearline on npm

Read the source

llms.txt

MIT licence

Studio

Kynth Studios

© 2026 Tearline. MIT licensed — free forever. A Kynth Studios project. The studio behind Agentwire, BreachProbe and CiteRank.

A Kynth Studios project

Tearline

Wrap any HTML in one tag and it prints as a receipt.

Product

Overview

Features

Playground

FAQ

Docs

Install

API reference

Exporting a PNG

Accessibility

DOM to PNG, explained

Receipt-style UI

Spotify receipt generators

Share images, four ways

Code

Source on GitHub

@kynth/tearline on npm

Read the source

llms.txt

MIT licence

Studio

Kynth Studios

© 2026 Tearline. MIT licensed — free forever. A Kynth Studios project. The studio behind Agentwire, BreachProbe and CiteRank.

A Kynth Studios project