I wanted visitors to be able to save a neat summary of their result as an image — the kind of thing you can share in a chat or keep for your records. The obvious way is a server endpoint that renders a PNG and sends it back. But my sites run on plain shared hosting with no build step and no image library, so I did it entirely in the browser with the canvas API. No server, no library, no extra dependency.
The idea is simple. A <canvas> element is a drawing surface you control with JavaScript. You draw text, lines and boxes on it, then call toDataURL() to turn the whole thing into a PNG data URL. Attach that URL to a link with a download attribute and the browser saves the image. The whole feature is about a hundred lines of vanilla JavaScript.
The first thing I got wrong was assuming the canvas would behave like a normal web page. It does not. There is no layout engine, no CSS, no wrapping text. Every line of text needs an x and a y coordinate, and you have to measure the width yourself with measureText() if you want anything right-aligned, like amounts in a column. I keep a running y cursor: draw a row, add the line height, draw the next one. It feels old-fashioned, but it is completely predictable once you accept it.
Sizing matters more than it looks. A canvas has two sizes: the size of the drawing surface in pixels, and the size it appears on the page. If they disagree, the browser stretches the drawing and everything goes blurry. For a receipt image I set the canvas to a fixed pixel size — say 600 wide — and never display it at a different size. I also draw at double resolution and scale down, so the text stays crisp on phone screens. A 600-pixel-wide canvas with a devicePixelRatio of 2 gives you a 1200-pixel-wide image, which looks sharp when shared.
Fonts are the other trap. If you draw text before the page font has loaded, the canvas falls back to a default font and your careful spacing breaks. I wait for document.fonts.ready before drawing, and I stick to system fonts anyway — no webfont download, no waiting, and the image looks the same on every device. Monospace for the numbers keeps the columns aligned without any measuring tricks.
The receipt itself is just rows: a title, a divider line, label-and-value pairs, a total in bold, and a small footer line. I draw the divider with fillRect() — a one-pixel-high rectangle is a cleaner line than trying to stroke a path. For the total I draw a slightly thicker line above it and bump the font weight. That is the whole design. A receipt does not need gradients.
One detail worth keeping: do the drawing only when the user asks for the image. Drawing on every keystroke is wasted work and can stutter on a cheap phone. I keep the canvas hidden, build the image when the download button is tapped, and hand the browser a fresh data URL each time. The data never leaves the device, which is a genuine privacy win — nothing the user typed is sent to my server to make the picture.
There are limits. Canvas text cannot be selected or copied, and a screen reader cannot read the image, so I keep the same numbers visible as normal HTML on the page. The image is a convenience, not the only copy of the result. And if you need a print-perfect layout, a PDF library is still the better tool. But for a quick shareable summary, the canvas approach is hard to beat.
I use this pattern on my tool sites — ramrom.io is the downloader I built and run on plain PHP hosting — wherever a visitor might want to keep a result. It costs nothing to run, it cannot break when a third-party service goes down, and it works offline once the page has loaded. Sometimes the simplest tool in the browser is the right one.
Top comments (0)