# Tearline
> One custom element. Wrap any HTML in `` and it renders as a thermal
> receipt — torn edge, barcode, receipt type — then exports itself as a PNG.
> Zero dependencies, no build step, MIT.
Tearline is a single-file web component (a custom element). It is not a
template language, not a canvas library, and not an image service. The receipt
is CSS wrapped around your own markup, so the text stays real text in the light
DOM: selectable, searchable, translatable, and read by screen readers in
document order.
## Load it
```html
Meridian
Cortado · 4.25
```
Verified reachable (HTTP 200) on 2026-07-31. There is no npm package to install
today — `npm i tearline` does not resolve, and the script tag above is the
supported install path.
## Export it to a PNG
```js
const el = document.querySelector('tear-line');
await el.download('receipt.png'); // saves it
const blob = await el.toBlob(); // or handle it yourself
const url = await el.toDataURL();
```
The export serialises the rendered receipt into an SVG `` and
paints it onto a canvas. No dependency — but that sandbox cannot reach the
network, so an `
` inside the receipt must be a `data:` URI or the export
rejects with an explicit error rather than handing back a receipt with a hole
in it. Text and CSS are inlined automatically.
The technique is not specific to Tearline, and it has a second failure mode
worth knowing: per MDN, drawing cross-origin data onto a canvas without CORS
approval taints it, after which `toBlob()`, `toDataURL()` and `captureStream()`
throw a `SecurityError` instead of returning anything. A blank export and a
thrown export are different bugs with different fixes. Both are written up at
https://tearline.kynth.studio/dom-to-png, alongside how the zero-dependency
DOM-to-image packages on npm compare (figures fetched 2026-07-31).
## Attributes
- `width` (default `330`) — paper width in pixels.
- `seed` (default `1`) — any integer. The same seed always produces the same
torn edge and the same barcode, so the export matches what the user saw.
- `barcode` — the digits printed under the bars. Omit for no barcode. It is
decorative and is not a scannable Code 128.
- `tilt` (default `-1.15`) — rotation in degrees.
- `flat` — no rotation, no drop shadow. For embedding inside another layout.
- `animate` — prints out on first paint. 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. `name` defaults to `receipt.png`.
## Styling
`h1`, `h2`, `hr`, `p`, `small`, `strong`, `table`, `ul` and `ol` are styled for
you inside the receipt. Every one of those rules is `::slotted()`, which loses
to your own CSS, so anything can be restyled from the outside without a
specificity fight. Paper and ink are custom properties: `--paper`, `--ink`,
`--ink-strong`, `--ink-faded`, `--font`.
That override behaviour is not a Tearline convention — it is the cascade. Per
CSS Cascade and Inheritance Level 5 (fetched 2026-08-01), when two declarations
come from different encapsulation contexts, the declaration from the OUTER
context wins for normal rules, and the inner context wins for important ones.
A `::slotted()` rule inside a component is therefore a default that any
ordinary rule on the host page beats.
## The receipt look, without the component
The look is plain CSS and can be rebuilt from scratch: a narrow measure, a
monospace stack, 11.5px type on 1.62 line-height with `.04em` tracking,
`font-variant-numeric: tabular-nums` so price columns align, dashed `
`
rules, an SVG `feTurbulence` data URI laid over the paper at
`mix-blend-mode: multiply`, and a seeded `clip-path` polygon for the torn edge
(mostly straight, roughly one point in six a deeper nick). The full write-up,
with the shipped values and why a share-image widget belongs in a custom
element rather than a framework component, is at
https://tearline.kynth.studio/receipt-ui.
## Building a Spotify listening receipt
Tearline renders the receipt and exports the PNG. It does nothing about getting
the data out of Spotify, and that is the hard half. Per Spotify's quota-modes
documentation (fetched 2026-08-02), a newly-created app is in development mode,
where "Up to 5 authenticated Spotify users can use an app that is in development
mode" and the owner must hold a Spotify Premium account. Extended quota mode
lifts the cap, but since 15 May 2025 "Spotify only accepts applications from
organizations (not individuals)", with stated criteria including a registered
business entity and "a minimum of active users (at least 250k MAUs)".
So an individual building one today has three realistic routes: ship it for the
five allowed users, take the listening data from the user (a paste box, a CSV,
a personal-data export) instead of via OAuth, or make a receipt of something
that has no gatekeeper. The full write-up — including the single endpoint the
whole genre is built on, GET /me/top/{type} with time_range and a limit capped
at 50, and five compared ways to turn the rows into a PNG — is at
https://tearline.kynth.studio/spotify-receipt-generator.
## Frameworks
It is a custom element, so it works anywhere HTML does — React, Vue, Svelte,
Astro, or a plain script tag in a static file. React 19 passes unknown
attributes through, so the tag needs no wrapper.
## Pages
- [Home and live playground](https://tearline.kynth.studio/)
- [Documentation and full API reference](https://tearline.kynth.studio/docs)
- [Export a DOM element as a PNG — the technique, its limits, and the npm
alternatives](https://tearline.kynth.studio/dom-to-png)
- [Receipt-style UI on the web — the CSS behind the paper look, and why it is a
custom element](https://tearline.kynth.studio/receipt-ui)
- [Receiptify-style Spotify receipt generators — the API cap that stops most of
them, and five ways to make the
image](https://tearline.kynth.studio/spotify-receipt-generator)
MIT © Kynth Studios.