Tearline

Try it

Tearline

Playground

Docs

API

FAQ

Try it

Tearline

Playground

Docs

API

FAQ

Try it

Method

The paper is CSS.
There is no image.

what the look is made of

A receipt is one of the few UI looks that is entirely reachable with plain CSS, because everything that makes a receipt look like a receipt is a property that already exists. Narrow measure, monospace, small type on loose leading, dashed rules, tracked capitals. There is no illustration, no sprite and no font to license.

Four layers, in the order they matter. The paper is a background colour and a wide, uneven linear-gradient across the width — a warm falloff at both edges and a soft crease off-centre, which is what stops a flat rectangle reading as a card. The fibre is a single SVG feTurbulence as a data URI, laid over the whole element at mix-blend-mode: multiply and about a third opacity: it is grain, not texture, and at full strength it looks like a filter. The rules are ordinary <hr> elements with a dashed top border. The type does the rest, and it has its own section below.

the torn edge

The tear is a clip-path polygon, generated once. Fifty-eight steps across the top and the same across the bottom; each point sits a few pixels in, with roughly a one-in-six chance of a deeper nick. That ratio is the whole trick — paper ripped off a printer is mostly straight with occasional ragged bites, so an even zigzag reads as a decorative border rather than a tear.

Generate it from a seeded PRNG rather than from randomness. A receipt that reshuffles its own edge on every render is unsettling on screen, and — more practically — an export taken a frame later will not match the shape the reader was looking at.

receipt.css

receipt.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 1 — the paper itself */
.receipt {
width: 330px;
padding: 34px 26px 30px;
background-color: #f6f3ec;
color: #2b2724;
font-family: ui-monospace, Menlo, monospace;
font-size: 11.5px;
line-height: 1.62;
letter-spacing: .04em;
font-variant-numeric: tabular-nums;
}
/* 2 — the fibre: one turbulence, multiplied over */
.receipt::before {
content: ""; position: absolute; inset: 0;
opacity: .34; mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,…feTurbulence…");
}
/* 3 — the rules are ordinary <hr> elements */
.receipt hr {
border: 0; margin: 13px 0;
border-top: 1px dashed rgba(40,36,33,.42);
}
/* 4 — headings print in tracked caps */
.receipt h1 {
text-align: center; text-transform: uppercase;
letter-spacing: .20em; font-weight: 700;
}

These are the shipped values, read out of the component served at /tearline.js on 1 August 2026 — not illustrative ones. One line is an addition rather than a quote: font-variant-numeric is not currently set by the component and should be. Copy the block above and you have the look without loading anything.

Type

Monospace does
most of the work.

property · value · why

font-familya monospace stackA thermal printer has a fixed character cell, so every receipt you have ever held is monospaced. This is the single decision that makes the look read; get it wrong and no amount of paper texture rescues it.
font-variant-numerictabular-numsPer MDN, tabular figures are the set where numbers are all the same width, so they align like a table. Prices in a right-hand column stop jittering line to line — the difference between a receipt and a list of numbers.
letter-spacing.04em body · .20em headingsThermal heads over-ink slightly and the paper wicks, so real receipt type sits looser than screen type. Body gets a hair of tracking; the shop name gets a lot, which is what sells the caps.
font-size11.5px, line-height 1.62Small type on a narrow measure. The generous leading is doing the work — a receipt is mostly whitespace between short rules, and tight leading reads as a terminal instead.
text-transformuppercase on headings onlyUppercase everything and it becomes unreadable rather than authentic. The header and the section labels are capitalised; the line items are not.
text-shadow0 0 .55px, ink-colouredA sub-pixel bloom in the ink colour. Thermal ink is never a crisp vector edge, and this is the cheapest approximation of that — it survives a 2× PNG export, where a blur filter would not.

the one to get right

If you only take one line from this page, take font-variant-numeric: tabular-nums. Most monospace stacks give it to you already, but the moment someone overrides the font — and on a share image built from a listening history, a spend summary or a sports scoreline, someone always does — proportional figures come back and the right-hand column starts wobbling. MDN describes tabular figures as the set where numbers are all the same size so they align like a table, which is exactly the job.

what not to reach for

Skip the crumple-paper photograph, the drop-shadowed cardstock and the 3D fold. They are three different aesthetics wearing a receipt costume, and none of them survives being rasterised into a 1,200px-wide share image — the detail that sold the effect at full size turns to mush at export scale.

The same goes for a real barcode. A decorative row of varied bars reads correctly at a glance; a scannable Code 128 that encodes nothing meaningful invites someone to scan it and file a bug. Vary the bar widths, print digits underneath, and say in your docs that it is decoration.

Packaging

One tag.
Why a custom element.

what the platform gives you for free

a hyphen in the nameMDN: the name "must start with a lowercase letter, contain a hyphen, and satisfy certain other rules". This is what keeps custom elements from ever colliding with a future built-in tag — and it is why every share-image widget you have seen is <something-something>.
observedAttributesA static array of the attributes you want change notifications for. MDN notes that if the element's HTML declaration includes an observed attribute, attributeChangedCallback() fires after the attribute is initialised, when the declaration is first parsed — so the same code path handles the first render and every later change.
attachShadowThe look goes in a shadow tree and stops there: page CSS does not reach into it, and its CSS does not leak out. For a widget that has to look identical on someone else's site, that is the entire point.
the light DOM stays realSlotted content is still your markup, in the document, in order. It stays selectable, searchable, translatable and readable by a screen reader — which a <canvas> or an <img> is not. The picture is the export, not the page.

A share-image widget is the case custom elements were designed for. It has a hard visual contract, it is dropped into pages whose CSS you will never see, and it has to work the same in React, Vue, Svelte, Astro and a static file with a script tag. A framework component gives you one of those; a custom element gives you all five, because it is just HTML.

element.js

element.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Receipt extends HTMLElement {
static observedAttributes = ['width', 'seed'];
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
// fires on first parse, not just on change
attributeChangedCallback() { this.paint(); }
// the export lives ON the element
async toBlob({ scale = 2 } = {}) { /* … */ }
}
customElements.define('tear-line', Receipt); // the hyphen is required

encapsulation is not a cage

The usual objection to shadow DOM is that users cannot restyle it. For slotted content that is backwards, and the spec says so: in CSS Cascade 5's sorting order, when two declarations come from different encapsulation contexts, the outer context wins for normal rules and the inner context wins for important ones. So a ::slotted() rule inside the component is a default that any ordinary rule on the host page beats, with no specificity fight and no !important arms race. Style the parts you want; the rest stays styled.

The other reason to package it this way: the export belongs on the element. A component that renders the picture and a separate library that rasterises it are two things to keep in sync, and the second one has to be told how to find the first. Put toBlob() on the element and the widget owns its own output. How that export actually works — and the two ways it fails — is written up at export a DOM element as a PNG.

Sources

Every claim here.
Fetched, not remembered.

checked 1 August 2026

developer.mozilla.org — Using custom elementsThe valid-name rule (lowercase start, must contain a hyphen), customElements.define(), and the timing of attributeChangedCallback() on first parse. Fetched 1 August 2026.
w3.org — CSS Cascade and Inheritance Level 5The tree-context criterion in cascade sorting order: between encapsulation contexts, the declaration from the outer context wins for normal rules, and the inner context wins for important rules. Fetched 1 August 2026.
developer.mozilla.org — font-variant-numerictabular-nums "activating the set of figures where numbers are all of the same size, allowing them to be easily aligned like in tables", mapping to the OpenType tnum feature. Fetched 1 August 2026.
tearline.kynth.studio/tearline.jsThe shipped implementation every measurement on this page was read out of — the paper CSS, the seeded tear polygon and the element class. 12.7 KB, unminified, HTTP 200 on 1 August 2026.

and the working version

Everything above is buildable from scratch — that is the point of writing it out. If you would rather not, Tearline is the same technique as one tag: wrap your markup in <tear-line> and it renders as the receipt and exports itself as a PNG. Zero dependencies, no build step, MIT. The full attribute and method reference covers the rest, and the playground on the home page edits a live one.

Spec behaviour decays more slowly than a version number, but it does decay — cascade rules get revised and browser support moves. Each claim above carries the date it was read, and the source link 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

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