Tearline

Try it

Tearline

Playground

Docs

API

FAQ

Try it

Tearline

Playground

Docs

API

FAQ

Try it

Docs

Load one file.
Wrap anything.

the whole install

One ES module, no build step, no peer dependencies and no server. Load it with a <script type="module"> tag and the browser registers a custom element called tear-line. Everything after that is markup you already know how to write — headings, rules, tables, lists — and the paper is CSS wrapped around it rather than a picture of it.

what it is not

Not a template language, not an image service, and not an ESC/POS driver — it does not talk to a physical thermal printer. It renders the look of a till receipt in the DOM, and hands you a PNG of it.

index.html

index.html
1
2
3
4
5
6
7
8
9
<script type="module" src="https://tearline.kynth.studio/tearline.js"></script>
<tear-line seed="7" barcode="047320260726">
<h1>Meridian</h1>
<small>Coffee & Provisions</small>
<hr>
<p>Cortado · 4.25</p>
<p><strong>Total · 4.25</strong></p>
</tear-line>

Reference

Six attributes.
Three methods.

attributes

width330Paper width in pixels.
seed1Any integer. The torn edge and the barcode are both drawn from this one number through a deterministic generator, so the same seed always produces the same paper — and the PNG matches what the visitor was looking at. Leave it out and every render tears differently, which is fine for a playground and wrong for an order confirmation.
barcode—The digits printed under the bars. Omit for no barcode at all. Decorative: it is not a scannable Code 128 and does not pretend to be.
tilt-1.15Rotation in degrees.
flat—Present: no rotation and no drop shadow. For embedding the receipt inside another layout rather than floating it on a page.
animate—Present: the receipt prints out on first paint, like paper feeding from a till. Skipped entirely under prefers-reduced-motion. The duration reads --dur, which defaults to 1.1s.

reacting to changes

width, seed, barcode and tilt are observed — set any of them on a live element and the paper redraws. flat and animate need no observer because they are matched by CSS on the host, so toggling them takes effect on the next frame either way.

knowing when it has painted

The element sets data-ready on itself one frame after the paper, the tear and the barcode are all in place. Gate your own fade-in on that attribute and nobody ever sees a half-drawn receipt.

methods

toBlob({ scale, padding })Resolves to a PNG Blob. scale defaults to 2, so a 330px receipt comes back 660px wide. padding defaults to 44 and exists because the drop shadow spreads past the element's own box — drop it to 0 alongside the flat attribute for a tight crop.
toDataURL({ scale, padding })The same image as a data: URL, for dropping straight into an <img> or a share sheet.
download(name, { scale, padding })Renders and saves it. name defaults to receipt.png.

custom properties

--paper#f6f3ecPaper colour, under the fibre texture and the falloff.
--ink#2b2724Body text.
--ink-strong#1a1715Headings and <strong>.
--ink-faded#6a635c<small>.
--fontui-monospace, …The receipt's type stack. Everything inside the paper inherits it.
--dur1.1sPrint-out duration, only read when the animate attribute is present.

Export

A PNG,
made in the browser.

how it works

The rendered receipt is serialised into an SVG <foreignObject> and painted onto a canvas. That is the whole mechanism, and it is why there is no dependency: no html2canvas, no headless browser, no screenshot endpoint. The shadow DOM is flattened, the slotted light DOM is inlined, and the component's own stylesheet is rewritten against the classes that survive the flattening.

the one caveat, stated plainly

A foreignObject is sandboxed: it cannot fetch anything over the network. Text and styles are inlined for you automatically, but an <img> inside the receipt must be a data: URI or it will not survive. When that happens the export rejects with an explicit error naming the cause, rather than quietly handing you a receipt with a hole in it. Fonts are subject to the same rule — a webfont that has not loaded falls back inside the export.

None of that is specific to Tearline — it falls out of how browser-side rasterisation works at all. Export a DOM element as a PNG walks through the technique on its own, including the tainted-canvas rule that throws instead of returning a blank image, and how the zero-dependency packages on npm compare.

making the image match the screen

Set a seed. The tear and the bars come out of it, so the exported PNG is the same paper the visitor was looking at when they pressed the button. Without one, the export re-renders a different tear and the share image quietly stops being a picture of what happened.

share.js

share.js
1
2
3
4
5
6
7
8
const el = document.querySelector('tear-line');
// saves it straight to the user's downloads
await el.download('receipt.png');
// or take the bytes and do your own thing
const blob = await el.toBlob({ scale: 3 });
const url = await el.toDataURL();

Styling

Opinionated defaults.
No specificity fight.

what is styled for you

h1, h2, hr, p, small, strong, table, ul and ol arrive looking like receipt type — centred uppercase headings, dashed rules, tight monospace rows — without you writing a line of CSS.

and how to override it

Every one of those rules is written with ::slotted(), which loses to your own author styles by design. So a plain selector from the outside wins — no !important, no wrapper class, no fighting the shadow boundary. Paper and ink are custom properties on the host, so a whole retheme is five declarations.

receipt.css

receipt.css
1
2
3
4
5
6
7
8
9
10
tear-line {
--paper: #f6f3ec;
--ink: #2b2724;
--ink-strong: #1a1715;
--ink-faded: #6a635c;
--font: ui-monospace, Menlo, monospace;
}
/* your own rules beat the built-in ones */
tear-line h1 { letter-spacing: 0; }

Frameworks

It is a custom element.
So it goes anywhere.

react

React 19 passes unknown attributes straight through to the DOM, so <tear-line seed={7}> works with no wrapper and no ref dance. Import the module once, at the top of your app, for its side effect — it registers the element and guards against double registration itself.

vue, svelte, astro

All three render custom elements natively. Vue wants the tag marked as a custom element in its compiler options so it stops warning about an unknown component; Svelte and Astro need nothing.

server rendering

The receipt is drawn in the browser, so a server-rendered page ships the markup and paints the paper on hydration. Your content is in the HTML either way — which is the point of it being real elements rather than a canvas. Gate any fade-in on data-ready and the swap is invisible.

content security policy

The component injects its own stylesheet into its shadow root, and the export builds a data: image URL. A strict CSP therefore needs img-src data: for the export to rasterise.

Accessibility

Real text.
Not a picture of text.

what that buys you

Your markup stays in the light DOM, so the receipt is selectable, searchable, translatable and read by screen readers in document order. Headings stay headings, tables stay tables, links stay links. A canvas-based receipt loses all of that the moment it paints, and a screenshot never had it.

motion

The print-out reveal is inside a prefers-reduced-motion: no-preference query, so it is not softened under a reduced-motion preference — it never runs at all.

contrast

The default paper and ink are a warm cream and a soft near-black, which is what a real till receipt looks like and is not always what an audit wants. --ink and --paper are exposed exactly so you can push the ratio past the look. The faded and strong inks are separate properties, so you can lift the quiet text without flattening the hierarchy.

the barcode

Decorative, and marked as such — it carries no text alternative because there is nothing to announce. The digits under it are real text and are read normally.

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

Code

Read the source

llms.txt

MIT licence

Studio

Kynth Studios

© 2026 Tearline. MIT licensed — free forever.

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

Code

Read the source

llms.txt

MIT licence

Studio

Kynth Studios

© 2026 Tearline. MIT licensed — free forever.

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

Code

Read the source

llms.txt

MIT licence

Studio

Kynth Studios

© 2026 Tearline. MIT licensed — free forever.

A Kynth Studios project