Any HTML.
Printed.
One custom element renders whatever you wrap in it as a thermal receipt — then hands your users a PNG of it.
One custom element renders whatever you wrap in it as a thermal receipt — then hands your users a PNG of it.
Cortado · 4.25
Total · 4.25
Features
No canvas API, no template language, no image service. Markup goes in, paper comes out.
Headings, rules, tables, lists, images. If it renders in HTML it prints on paper — you are not learning a receipt DSL.
One call turns the receipt into an image, at any scale, ready to post.
The paper is styling wrapped around your markup, so everything a browser does with text still works.
The torn edge and the barcode are generated from a seed, so a receipt renders identically every time — and the export matches what your user actually saw.
It is a custom element. React, Vue, Svelte, Astro, or a script tag in a static file.
No build step, no runtime, no peer deps. One file you can read in an afternoon.
Playground
This is the real element, loaded the way the docs tell you to load it. Edit anything.
<h1>Meridian</h1>
<p><small>WRAP ANYTHING</small></p>
<hr>
<table>
<tr>
<td>1</td>
<td>Cortado</td>
<td align="right">4.25</td>
</tr>
<tr>
<td>1</td>
<td>Sourdough</td>
<td align="right">3.50</td>
</tr>
</table>
<hr>
<table>
<tr>
<td><strong>TOTAL</strong></td>
<td align="right"><strong>7.75</strong></td>
</tr>
</table>
<hr>
<p><small>THANK YOU</small></p>
tag
dependencies
build steps
licence
Install
drop in a script tag
That is the install. It is an ES module served from this origin, so there is no package to add, no bundler to configure and no build step — the custom element registers itself on load and every <tear-line> on the page upgrades in place.
Rendering to an image uses an SVG foreignObject, which is sandboxed and cannot fetch over the network. Text and styles are inlined for you, but an image inside the receipt must be a data: URI. The export throws with a message saying so rather than handing you a receipt with a hole in it.
then wrap anything
attributes
width | 330 | Paper width in pixels. |
seed | 1 | Any integer. The same seed always produces the same torn edge and the same barcode, so a receipt renders identically every time — and the export matches what the user saw. |
barcode | — | The digits printed under the bars. Omit for no barcode. Decorative: it is not a scannable Code 128 and does not pretend to be. |
tilt | -1.15 | Rotation in degrees. |
flat | — | No rotation, no shadow. For embedding inside another layout. |
animate | — | Prints out on first paint, like paper feeding from a till. Skipped under prefers-reduced-motion. |
methods
toBlob({ scale }) | Resolves to a PNG Blob. Scale defaults to 2. |
toDataURL({ scale }) | Resolves to a PNG data URL. |
download(name, { scale }) | Saves the PNG. |
accessibility
The receipt is real text in the light DOM — not a canvas and not an image. It is selectable, searchable, translatable, and read by screen readers in document order, because the paper is styling wrapped around your markup. Ink and paper are exposed as --ink and --paper so you can raise contrast past the default receipt look.
FAQ
Everything you need to know before you install it.
One custom element. Wrap any HTML in <tear-line> and it renders as a thermal receipt — torn edge, barcode, receipt type — and exports itself as a PNG. It is not a template language and not an image service; it is styling wrapped around your own markup.
Anything a user might want to keep or post: an order summary, a workout, a year in review, a booking, a set list, a diff. Receipts are shareable in a way that a screenshot of a table is not, which is the whole reason this exists.
No, and it does not pretend to. The bars are generated from the seed for looks. If you need a scannable code, render a real one and put it inside the receipt as a data: URI image.
Yes, as long as you set a seed. The torn edge and the barcode come from it, so the same seed always produces the same paper. Without one you get a new tear on every render — fine for a playground, wrong for an order confirmation.
It is real text in the light DOM, not a canvas and not an image, so it is selectable, searchable, translatable, and read by screen readers in document order. Your headings stay headings and your tables stay tables. The print-out animation is skipped entirely under prefers-reduced-motion.
It is a custom element, so it works anywhere HTML does — including a plain script tag in a static file. React 19 passes unknown attributes through, so the tag needs no wrapper.
Nothing. MIT, zero dependencies, one file. There is no account, no key and no server — the export happens in the browser.
One tag, zero dependencies, no build step. MIT — free forever.
One tag, zero dependencies, no build step. MIT — free forever.
One tag, zero dependencies, no build step. MIT — free forever.