← Learn · Build · 13 November 2026 · 5 min

Receipt: A Grocery Shop That Prints Itself as You Scroll

How I built a corner grocer whose whole shop is one thermal receipt coming out of a printer as you scroll: a Codex printer, sixteen Codex groceries dithered to thermal dots, and a page where only the paper moves. Prompts and code inside.

The output, liveavivashishta.com/lab/receipt
Open the live page ↗

A grocery receipt is already a product list: name, price, one line each. So I made a shop that is a receipt. A printer sits at the bottom of the screen, and as you scroll the shop prints out of it, line by line, with a little picture of every product printed in thermal dots. Tap Add and it prints on your own receipt; tear that off to pay. Print it at avivashishta.com/lab/receipt.

The short version: Codex makes one photo of a thermal printer and one product shot of each grocery. A script cuts the printer out with macOS Vision and dithers every grocery to pure black dots, the only thing a thermal printer can print. On the page, the printer is fixed to the bottom of the screen and the receipt sits in a window that ends exactly at its slot. The scroll pushes the paper up out of it.

Step 1: the printer (Codex)

It has to be straight on, with the slot along the top, so paper can come up out of it:

A compact white thermal receipt printer photographed perfectly straight-on from the front at eye level, centered, orthographic, symmetric. Its paper exit slot is a thin dark horizontal opening running along the whole TOP edge of the printer, with a small serrated metal tear bar just behind it. Rounded matte white plastic body, one small round power button with a tiny green light on the front, rubber feet. No paper coming out. Pure seamless white background (#FFFFFF), soft even studio light, a very soft shadow under it, photoreal product photo. No text, no logos. Landscape 3:2, 1536x1024.

I measured the slot once on the photo (its two ends and its height) and the prep script turns that into fractions of the cut-out printer. Those three numbers place the paper.

Step 2: groceries, then dots

Sixteen groceries, one prompt each. "High contrast" and "bright even studio light" matter here, because the next step throws away every grey:

A grocery e-commerce product photo of a round crusty sourdough loaf with a deep scored ear. The product alone, unbranded, no text, no labels with words, no logos, centered with generous margin, three-quarter view, on a pure seamless white background (#FFFFFF), bright even studio light, high contrast, a very faint contact shadow only, photoreal, crisp. Square 1:1, 1024x1024.

A thermal printer has one colour: a dot or no dot. So each photo is made small (150 pixels), grey, pushed in contrast, and dithered with Floyd-Steinberg: every pixel becomes black or nothing, and the error it made is passed on to its neighbours, so the greys come back as patterns of dots.

const old = g[i], v = old < 128 ? 0 : 255, e = old - v;
if (v === 0) px[i * 4 + 3] = 255; // a black dot; everything else stays clear
g[i + 1] += (e * 7) / 16;          // pass the error on: right,
g[i + w - 1] += (e * 3) / 16;      // down-left,
g[i + w] += (e * 5) / 16;          // down,
g[i + w + 1] += e / 16;            // down-right

The page shows them at their own pixels with image-rendering: pixelated, so the dots stay crisp instead of blurring back into grey. A two-colour PNG of a loaf of bread is about 4 KB.

bun scripts/prepare-receipt.mjs ./raw

Step 3: only the paper moves

The page scrolls like any page, but nothing on it moves except the paper. The printer is fixed to the bottom. Above its slot is a window (overflow: hidden) whose bottom edge is exactly the slot. The receipt sits in that window, and each frame it is pushed up by how far you have scrolled, plus what the printer printed on its own when the page opened:

const target = innerHeight * FIRST * burst + scrollY;
fed += (target - fed) * 0.35;
paper.style.transform = `translate3d(0, ${roll.clientHeight - fed}px, 0)`;

So the store's header is the first thing out and rises to the top, and the newest line is always at the slot, as on a real receipt. The page is exactly as tall as the receipt, so scrolling to the bottom prints all of it.

The opening print comes out in nine short bursts instead of one smooth slide, because that is how a thermal printer sounds and moves.

The small things

  • Your receipt on the right prints a line for every Add, with a minus to take one off, free delivery over ₹500 and a total.
  • Tear off and pay rips your receipt off with a jerk and flies it away, then prints PAID with an order number.
  • The live date and time are printed in the header, and the cashier is "your scroll".
  • Sound (off until you turn it on) ticks the paper feed as it prints.

Build your own

Build a shop page that is one thermal receipt (React, no libraries). Assets: printer.webp, a cut-out straight-on photo of a receipt printer with its slot along the top (its ends and height as fractions of the image), and one dithered 1-bit PNG per product. Fix the printer to the bottom centre of the screen. Above its slot, put a 100vh tall window with overflow hidden whose bottom edge is the slot and whose width is a little less than the slot. In it, the receipt: off-white paper, monospace uppercase text, a header, aisles separated by dashed rules, and per product its dithered image (image-rendering: pixelated), name with dot leaders to the price, a note and an Add button.

Each frame, translate the receipt so its bottom is at the slot minus how much has been fed, where fed eases towards scrollY plus an opening amount that prints in nine bursts on load. Make the page as tall as the receipt. Show a second small receipt on the right with the bag (zig-zag bottom edge via a mask), a total, and a Tear off and pay button that flies it away and prints PAID.