GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Receipt: 9 Thermal Print and Tear-Off Scenes

A css receipt layers print-head shiver, paper curl and a rolling total — scene 01 adds a tremor to each line step, and scene 06 shades unwinding paper pixel by

The shop name, the items and the prices are invented to fill the paper, and what the nine scenes isolate is the moment a slip gets printed, torn, scanned, faded or split between people.

Print, Tear and Scan All Nine Receipts Right on This Page

Auto-plays · click a tile to jump to its section · all nine in one zip

CSS Receipt: 9 Paper Scenes Built From Line Feeds, Tear Tests and Cylinder Shading

Monospaced type on a white card looks like a receipt, yet what makes it feel like one is how the paper comes out of the slot and how it comes apart. That is why scenes 01 through 08 compute the paper on a 2D <canvas>, and only scene 09 moves real DOM buttons with transform. None of the nine stylesheets holds a transition or a keyframes animation; they only set the stage ratio, button shapes, focus rings and touch-action.

One fixed-step clock and one seeded generator run under the scenes

Each scene stores the timestamp that requestAnimationFrame hands over, slices the elapsed time into 16ms steps (FIXED_DT) for step(), and draws once per frame. Time added in a single frame is capped at 100ms and a frame runs six steps at most, so a stalled frame does not make the paper leap. Randomness comes from one seeded lcg, called only in 02 (the break threshold of each perforation tooth) and 04 (bar widths and digits); no index.html contains Math.random, Date.now, fetch or localStorage, so a reload prints the same slip in the same order.

The shop, the items and the 0.1 tax rate are made up

Moonlight Shop and the items Milk, Bread, Apples, Eggs and Tangerines, along with every amount beside them, are placeholder text chosen for the scenes. TAX_RATE 0.1 is just a number that lets the math show, and it is not tied to any country's tax rules. If you move a scene into a real checkout, swap the amounts and the tax calculation for your own service's rules.

Motion the renderer recorded for each receipt scene

The capture window was 480×300, 24 frames over two seconds, each compared with the one before it, so moved frames are counted out of 23.

Scene Covered area Mean intensity Moved frames
01 Line-by-line print with shiver 23.6% 35.6 23/23
02 Tear along the perforation 26.9% 118.4 20/23
03 Zigzag edge and curl 36.5% 65.8 23/23
04 Barcode scan beam 15.1% 66.9 23/23
05 Rolling total 8.6% 103.6 23/23
06 Roll cylinder shading 32.6% 51.3 23/23
07 Thermal fade 40.5% 30.7 18/23
08 Spike pendulums 41.1% 83.4 23/23
09 Split bill 37.7% 138.5 23/23

Scene 05 is the only one under 10%, since the part of the screen that changes is a handful of digit wheels. Scene 07 reads 18/23 because the pause after the text has faded and before the next print fell inside the two seconds, and scene 02 reads 20/23 for the same reason: the strip had sprung back or dropped away and the scene was waiting for fresh paper.

Screens where a moving receipt earns its place

A receipt css template usually stops at one static paper card, and this set picks the moments when that paper moves instead. For an order summary that should print line by line after an in-store payment, start from 01; for a "get your receipt" step that feels torn by hand, start from 02. Decorative coupon or receipt-card edges fit 03, a confirmation that a return or refund went through fits 04, and a cart total or payment summary fits 05. A screen that waits on printing suits 06, an archive of older purchases suits 07 and 08, and a group-expense screen that divides a bill per person can grow out of 09.

Code for Each Technique Behind a CSS Receipt Design

Each snippet is an unbroken run of 8 to 25 lines lifted from the <script> in that scene's index.html, indentation included. Function and constant names match the source, so after unzipping, a search for the same name lands you on the code around the snippet.

01A Receipt Print Scene Where the Paper Steps One Line at a Time and Shivers Slightly Under the Print Head

Each line is revealed from left to right at 0.3 opacity over 180ms (LINE_MS), darkens over 160ms once the line is done, and then the paper feeds FEED_PX 14px during 140ms (FEED_MS) while shiverAt adds a sideways tremor. A line's height comes only from the gap between the feed value when it was printed and the feed value now, so all nine lines rise as one block with no per-line motion. The tremor is SHIVER_PX 2.4 multiplied by (1 − k), which keeps the real peak near 2.2px, and the render logged 23.6% covered area, intensity 35.6 and 23/23 frames.

A Receipt Print Scene Where the Paper Steps One Line at a Time and Shivers Slightly Under the Print Head
Open in a new tab

01 · A Receipt Print Scene Where the Paper Steps One Line at a Time and Shivers Slightly Under the Print Head — Measured area 23.613% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FEED_PXLINE_MSSHIVER_PXPAPER_W
    // sideways shiver during the feed that follows — strong at the start, dying out toward the end
    function shiverAt(k) {
        return SHIVER_PX * Math.sin(k * Math.PI * 7) * (1 - k);
    }
    // vertical position of one line — moves away from the head by the gap between the feed when printed and the feed now
    function rowY(printedScroll, scrollNow) {
        return HEAD_Y - 9 - (scrollNow - printedScroll);
    }

02A Scene Where Pulling the Paper Below the Perforation and Letting Go Tears It Off Past a Set Distance and Springs It Back Short of It

The perforation has 21 teeth (STRIP_W 210 ÷ TOOTH_PX 10), and seed 461 gives each one its own break distance between 0.45 and 1.0 times TEAR_PX, so they snap one by one at different moments while you drag. The tear verdict ignores the teeth: release() decides with a single pull ≥ TEAR_PX when you let go, then drops the strip with GRAVITY 2600 or winds it back on a spring with SPRING_K 190 and damping 17. Measured covered area is 26.9% at intensity 118.4, and the idle wait for new paper after a spring-back leaves 20/23 moved frames.

A Scene Where Pulling the Paper Below the Perforation and Letting Go Tears It Off Past a Set Distance and Springs It Back Short of It
Open in a new tab

02 · A Scene Where Pulling the Paper Below the Perforation and Letting Go Tears It Off Past a Set Distance and Springs It Back Short of It — Measured area 26.888% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

TEAR_PXGRAVITYSPRING_KTOOTH_PX
    const rnd = lcg(461);
    for (let i = 0; i < 40; i += 1) {
        toothBreak.push(0.45 + 0.55 * rnd());
    }
    function teethCount() { return Math.floor(STRIP_W / TOOTH_PX); }
    // teeth snap one by one as the pull grows — each tooth breaks at its own distance
    function intactTeeth(d) {
        let n = 0;
        for (let i = 0; i < teethCount(); i += 1) {
            if (d < TEAR_PX * toothBreak[i % 40]) {
                n += 1;
            }
        }
        return n;
    }

03A Paper Texture Scene With a Zigzag Edge and a Curled Corner Where a Different Shadow Falls on Each Column

The paper is treated as a surface that stays flat in the middle and tilts by u × 0.62 toward both ends, and each of 100 columns 3px wide is painted with (1 − dot) × SHADE_MAX from the dot product of its normal and the light direction. The light swings SWAY_DEG 46° either side of LIGHT_DEG 38° once every 2.4 seconds, and at the 84° end the rightmost column reaches the 0.46 cap, so that side darkens along with the curled corner's shadow. This scene recorded 36.5% covered area, intensity 65.8 and 23/23 frames.

A Paper Texture Scene With a Zigzag Edge and a Curled Corner Where a Different Shadow Falls on Each Column
Open in a new tab

03 · A Paper Texture Scene With a Zigzag Edge and a Curled Corner Where a Different Shadow Falls on Each Column — Measured area 36.52% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TOOTH_NCURL_PXLIGHT_DEGSHADE_MAX
    // current light angle (degrees) — in auto mode it keeps swinging around the base angle
    function lightDeg() {
        if (manual) {
            return manualDeg;
        }
        return LIGHT_DEG + SWAY_DEG * Math.sin((clock / SWAY_MS) * Math.PI * 2);
    }
    // shade strength of one column — treats the paper as a gently curled surface and uses the dot product of normal and light
    function shadeAt(col, deg) {
        const u = col / SHEET_W * 2 - 1;
        const slope = u * 0.62;
        const nx = -slope;
        const nz = Math.sqrt(1 - Math.min(0.99, slope * slope));
        const rad = deg * Math.PI / 180;
        const lx = Math.sin(rad);
        const lz = Math.cos(rad);
        const dot = clamp(nx * lx + nz * lz, 0, 1);
        return (1 - dot) * SHADE_MAX;
    }

04A Barcode Scan Where Seed-Made Bar Widths Light Up in Turn as a Beam Sweeps and a Confirm Mark Turns On

lcg(SEED) draws the widths of 46 bars (BAR_N) from 1 to 4 units, and for each point in a 336×118 pixel buffer the distance d to the beam gives exp(−d² / (2·GLOW_PX²)), mixing ink bars up to 0.9 toward pink and paper up to 0.55 toward white. The beam position gets 5·sin(y/15 + clock/140) added, so one column glows differently at each height, and when the beam reaches the end the confirm dot turns mint, its radius swelling to 1.5 times and quickly easing back to size (1 + 0.5·exp(−t/160)). Pushing the buffer with putImageData every frame, it measured 15.1% covered area, intensity 66.9 and 23/23 frames.

A Barcode Scan Where Seed-Made Bar Widths Light Up in Turn as a Beam Sweeps and a Confirm Mark Turns On
Open in a new tab

04 · A Barcode Scan Where Seed-Made Bar Widths Light Up in Turn as a Beam Sweeps and a Confirm Mark Turns On — Measured area 15.149% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BAR_NBEAM_MSGLOW_PXSEED
    function paintBars(t) {
        const data = img.data;
        const sig = 2 * GLOW_PX * GLOW_PX;
        for (let y = 0; y < BH; y += 1) {
            const bx = beamX(y, t);
            const fall = 1 - 0.18 * (y / BH);
            for (let x = 0; x < BW; x += 1) {
                const dx = x - bx;
                const g = Math.exp(-(dx * dx) / sig) * fall;
                const ink = inkAt[x] === 1;
                const base = ink ? INK : PAPER;
                const tint = ink ? PINK : [255, 255, 255];
                const m = ink ? g * 0.9 : g * 0.55;
                const i4 = (y * BW + x) * 4;
                data[i4] = base[0] + (tint[0] - base[0]) * m;
                data[i4 + 1] = base[1] + (tint[1] - base[1]) * m;
                data[i4 + 2] = base[2] + (tint[2] - base[2]) * m;
                data[i4 + 3] = 255;
            }
        }
        offCtx.putImageData(img, 0, 0);
    }

05A Total Line Where Digits Roll Vertically as Items Are Added and the Tax Row Follows One Beat Late

On each tick the total chases its target with speed = speed × INERTIA + (target − shown) × PULL_K, overshooting by 4.2% before coming back, and digitPos turns the ones wheel by the raw value while each higher wheel only advances during the last 0.1 when the wheel below passes 9.9. The tax row does not read the computed target; it takes 10% of the total currently on screen through a lag filter whose time constant is DELAY_MS 260; add a 1,200 item and, as the total passes 960, the tax reads about 52 instead of 96, yet the same filter flattens the overshoot (the tax peaks only 0.46% over its target), so the tax stays within ±0.5 from 1,584ms while the total stays there only from 2,368ms: the tax settles first. Only the digit wheels move, which gives the smallest covered area at 8.6%, yet intensity hits 103.6 and change showed in 23/23 frames.

A Total Line Where Digits Roll Vertically as Items Are Added and the Tax Row Follows One Beat Late
Open in a new tab

05 · A Total Line Where Digits Roll Vertically as Items Are Added and the Tax Row Follows One Beat Late — Measured area 8.624% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

DIGIT_HINERTIATAX_RATEDELAY_MS
    // position of one digit wheel — rolls only during the carry (when the wheel below passes 9.9)
    function digitPos(value, d) {
        if (d === 0) {
            return value;
        }
        const scale = Math.pow(10, d);
        const base = Math.floor(value / scale);
        const f = value / scale - base;
        return base + smooth((f - 0.9) / 0.1);
    }

06A Receipt Roll Scene Where Cylinder Shading Is Computed Per Pixel as Paper Unwinds and the Roll Radius Shrinks

radiusAt subtracts THICK × unwound length from the squared radius, so the roll goes from 58px to 40.2px by the end of one 2.6-second cycle and stops shrinking at the 0.4 × ROLL_R² floor. Each pixel of a 340×118 buffer takes the dot product of the cylinder normal (0, ny, √(1 − ny²)) with the light and adds a highlight of 0.25 × (N·H)^26, and THICK 4.8 is not a real paper thickness but a value inflated so the radius change stays visible. The roll recorded 32.6% covered area, intensity 51.3 and 23/23 frames.

A Receipt Roll Scene Where Cylinder Shading Is Computed Per Pixel as Paper Unwinds and the Roll Radius Shrinks
Open in a new tab

06 · A Receipt Roll Scene Where Cylinder Shading Is Computed Per Pixel as Paper Unwinds and the Roll Radius Shrinks — Measured area 32.6% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ROLL_RLIGHT_VECUNWIND_PXSHADE_GAIN
    // roll radius from the remaining paper length — the area (radius squared) shrinks in proportion to length
    function radiusAt(len) {
        return Math.sqrt(Math.max(ROLL_R * ROLL_R * 0.4, ROLL_R * ROLL_R - THICK * len));
    }
    // shade of one pixel — dot product of the cylinder normal (0, ny, nz) and the light direction
    function shadePixel(ny) {
        const nz = Math.sqrt(Math.max(0, 1 - ny * ny));
        const diff = Math.max(0, ny * ly + nz * lz);
        const hx = lx;
        const hy = ly;
        const hz = lz + 1;
        const hl = Math.sqrt(hx * hx + hy * hy + hz * hz);
        const spec = Math.pow(Math.max(0, (ny * hy + nz * hz) / hl), 26);
        return 0.42 + SHADE_GAIN * diff + spec * 0.25;
    }

07Thermal-Paper Text That Darkens Only Where Heat Touched and Fades Unevenly Over Time Following a Noise Map

A print head travels top to bottom for 700ms, adding HEAT_GAIN 1 of heat where glyphs sit and only HALO 0.24 on blank paper, below the CUTOFF threshold of 0.25, and afterward every point loses heat at decayAt(n). The map value n is value noise mixing large blobs at NOISE_SCALE 30 (weight 0.7) with fine grain (0.3), so heat 1.0 drops under the threshold anywhere from 0.58 seconds at the fastest spot to 1.53 seconds at the slowest, and the blotches appear. Looping over 280×300 points per tick, it came out at 40.5% covered area and intensity 30.7, with 18/23 moved frames because of the rest after a full fade.

Thermal-Paper Text That Darkens Only Where Heat Touched and Fades Unevenly Over Time Following a Noise Map
Open in a new tab

07 · Thermal-Paper Text That Darkens Only Where Heat Touched and Fades Unevenly Over Time Following a Noise Map — Measured area 40.473% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

HEAT_GAINFADE_RATENOISE_SCALECUTOFF
    // value noise — smoothly blends four lattice-point hashes
    function noise2(x, y) {
        const xi = Math.floor(x);
        const yi = Math.floor(y);
        const fx = smooth(x - xi);
        const fy = smooth(y - yi);
        const a = hash2(xi, yi);
        const b = hash2(xi + 1, yi);
        const c = hash2(xi, yi + 1);
        const d = hash2(xi + 1, yi + 1);
        return lerp(lerp(a, b, fx), lerp(c, d, fx), fy);
    }
    // fade speed at one spot (heat lost per second) — the higher the map value n (0 to 1), the faster it fades
    function decayAt(n) {
        return FADE_RATE * (1 + SPREAD * n);
    }
    // fade speed map — a different value per position (large blobs + fine grain)
    function buildRate() {
        for (let y = 0; y < H; y += 1) {
            for (let x = 0; x < PW; x += 1) {
                const n = 0.7 * noise2(x / NOISE_SCALE, y / NOISE_SCALE) + 0.3 * noise2(x / (NOISE_SCALE * 0.35) + 9, y / (NOISE_SCALE * 0.35) + 4);
                rate[y * PW + x] = n;
            }
        }
    }

08A Storage Scene Where Nine Receipts Hang Side by Side on a Rail and Sway as Pendulums of Different Lengths, the Shortest Settling First

The nine slips grow from 70px to 246px in 22px steps and swing under the angular acceleration −(GRAV / L)·sin θ − DAMP·(LEN_REF / L)·ω, so shorter slips swing faster and calm down sooner. Every 1,800ms an impulse that flips direction is applied in inverse proportion to length, and while the shortest slip (period 0.88 s) stops after about 3.2 seconds, the longest (period 1.64 s) needs about 8.3, so the next push arrives before the long ones come to rest. With 41.1% it covers the widest area of the nine, at intensity 83.4 and 23/23 frames.

A Storage Scene Where Nine Receipts Hang Side by Side on a Rail and Sway as Pendulums of Different Lengths, the Shortest Settling First
Open in a new tab

08 · A Storage Scene Where Nine Receipts Hang Side by Side on a Rail and Sway as Pendulums of Different Lengths, the Shortest Settling First — Measured area 41.086% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SHEET_NLEN_MINDAMPKICK
    // angular acceleration of one slip — longer is slower, shorter gets more air drag
    function accel(i, a, v) {
        return -(GRAV / len[i]) * Math.sin(a) - DAMP * (LEN_REF / len[i]) * v;
    }
    function kick(dir) {
        for (let i = 0; i < SHEET_N; i += 1) {
            vel[i] += dir * KICK * (LEN_REF / len[i]);
        }
        barVel += dir * 60;
    }
    function step() {
        clock += FIXED_DT;
        const dt = FIXED_DT / 1000;
        if (clock >= nextKick) {
            kick(sign);
            sign = -sign;
            nextKick = clock + KICK_MS;
        }
        for (let i = 0; i < SHEET_N; i += 1) {
            vel[i] += accel(i, ang[i], vel[i]) * dt;
            ang[i] += vel[i] * dt;
        }
        barVel += (-90 * barShift - 9 * barVel) * dt;
        barShift += barVel * dt;
    }

09A Split-Bill List Where Dragging an Item Into a Person's Slot Makes Each Slot Total Ease Toward Its New Value

When a chip is dropped, judge checks which of three slot rectangles holds the chip's center, and a spring with SPRING_K 240 and damping 19 carries it there, or back to where it came from, using transform alone. A slot total aims at round(subtotal × (1 + TAX_RATE)) and closes 1 − exp(−EASE_RATE·dt) of the gap per step, so a slot that receives one Bread reaches 90% of its 3,080 at 256ms and 99% at 512ms. Without any canvas, five moving buttons still produced 37.7% covered area, intensity 138.5 and 23/23 frames.

A Split-Bill List Where Dragging an Item Into a Person's Slot Makes Each Slot Total Ease Toward Its New Value
Open in a new tab

09 · A Split-Bill List Where Dragging an Item Into a Person's Slot Makes Each Slot Total Ease Toward Its New Value — Measured area 37.728% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SLOT_NTAX_RATESPRING_KEASE_RATE
    // verdict on release — which slot contains the chip center
    function judge(cx, cy) {
        for (let s = 0; s < SLOT_N; s += 1) {
            const r = slotRect(s);
            if (cx >= r[0] && cx <= r[0] + r[2] && cy >= r[1] && cy <= r[1] + r[3]) {
                return s;
            }
        }
        return -1;
    }
    function drop(i) {
        const s = judge(px[i] + CHIP_W / 2, py[i] + CHIP_H / 2);
        if (s < 0) {
            status.textContent = labelOf(i) + ' — ' + TXT_MISS;
            if (reduceQuery.matches) {
                snap();
            }
            return;
        }
        assign(i, s);
    }

Feed Distance, Tear Threshold, Fade Speed — Where One Value Changes How the Paper Feels

These numbers are not from render captures; they come from loading rowY, intactTeeth, decayAt, buildRate and buildBars from the scene sources into a script and computing with them. The method differs from the motion table above, so keep the two sets apart.

Raising FEED_PX from 14 to 30 pushes the header line off the top of the paper

At the moment all nine lines are printed, the first line moves from y 101 to −43 and the last from 213 to 197, and the distance between them grows from 112px to 240px. Seven of the nine lines stay on screen, and the shop name with the first divider slides out the top. The roughly 2.2px peak tremor stays put, so measured against line spacing it shrinks to less than half of what it was at 14px.

Raising TEAR_PX from 60 to 140 makes the 96px pull spring back

The demo's even-numbered pull of 96px clears the threshold at 60 and the strip falls, but at 140 it fails the test and springs back. Teeth still holding at 96px go from 0 to 9 of 21, three remain even at a 130px pull, and a 36px pull keeps all 21 instead of 14. Because each tooth's break point scales with TEAR_PX, this single value moves both how tough the paper feels and where the tear decision lands.

Raising FADE_RATE from 0.4 to 1.6 fades the print about four times faster

The median time for heat 1.0 to drop under CUTOFF goes from 0.86 to 0.22 seconds, the slowest spot from 1.53 to 0.38, and the fastest from 0.58 to 0.15. The noise map keeps its shape, so blotches form in the same places at the same proportions while the time axis folds to a quarter. The automatic cycle CYCLE_MS stays at 2.8 seconds, which means the blank-paper rest inside each cycle gets longer by the same amount.

Changing the barcode seed from 461 to 462 changes the bar count and the digits together

Seed 461 yields 154 ink columns and the number 472925035412; pressing "Next code" moves to seed 462, which gives 149 columns and 489092530559. Building twice from one seed reproduces identical bars and digits. The digits come from lcg(seed + 7), a separate sequence from the bars, so bar widths and digits do not encode each other.

Where the Receipt Breaks — Letting Go Just Short of the Threshold, an Overshooting Total, Reduced Motion

Any scene where a hand moves paper leaves gaps between what you see and what the code decides. Each case below was checked by recomputing the source formulas or following the input path; how long a frame takes on a given device was not measured.

Scene 02 springs back at 59px even with nearly every tooth snapped

Teeth snap one by one as the pull grows, but the tear verdict is a single pull ≥ TEAR_PX at release. Let go at 59px and the paper looks almost detached, yet the spring winds it back over 512ms and overshoots by 3.6px the other way before stopping. A release at 36px takes 496ms with a 2.2px overshoot, and a strip released at 96px leaves the bottom of the screen within 384ms.

Scene 02's keyboard path needs Enter to let go

With the canvas focused, ArrowDown pulls 12px and ArrowUp gives 12px back, but lifting the key does not release the strip; the verdict comes only when you press Enter. While fresh paper slides out, the strip cannot be grabbed. Once a person has touched it, the automatic demo stays off until a reload, and 05 and 09 likewise stop their auto run after a human input.

Scene 05 briefly shows a number larger than the target

Because the total follows with inertia, it passes the target by 4.2%. Heading for 11,600 it peaks near 12,090 before coming down, Heading for 1,200, it first enters the ±0.5 band at 1,648ms, slips out once more, and stays only from 2,368ms; heading for 13,500, it first enters at 2,592ms and stays from 3,184ms. If the slot shows a real amount, the in-between numbers reach the screen too, and with five wheels the leading digit is cut off past 99,999.

Scene 05's last tax digit rolls while straddling two numbers

The tax wheels receive the unrounded interpolated value as is. Until the value stops, the final digit sits halfway between two numbers. To have it settle on a clean figure, add one line that turns the value into an integer before it goes to the wheels.

Scene 03 gives drag and arrow keys different angle ranges

The automatic sway moves the light between −8° and 84°, but an angle set by dragging the canvas is clamped to ±60°, while arrow keys step 6° at a time up to ±90°. Dragging therefore cannot reproduce the 84° end of the auto range, and the keys can tilt to −90°, which the auto sway does not reach.

Scene 06 flattens the dark side once the light is pushed to an end

Shading is 0.42 + SHADE_GAIN × max(0, N·L), so a face that gets no light stays at 0.42, and the end caps are further multiplied by up to 0.55. Push the light to one end and the far side sticks to that floor, pressed nearly black. Pressing Pause stops only the roll; moving the pointer keeps changing the light.

Scene 07 leaves stains like letters if blank-paper heat crosses the threshold

HALO 0.24 on blank paper sits just under CUTOFF 0.25, so it warms faintly without turning into ink. Raise it to 0.25 or more and ink-colored stains appear where no glyph exists. The pixel loop visits 84,000 points per tick, more than twice the 336×118 and 340×118 buffers in 04 and 06.

Scene 08 has no limit on the swing angle

The restoring force is sin θ alone, with no clamp on the angle. The Tap button or a press on the canvas runs userKick(), which calls the same kick(sign) as the automatic impulse and then flips the sign, so rapid presses alternate left and right instead of stacking on one side. Each press also moves the next automatic impulse to 1.8 seconds after that moment, so the auto schedule stays away while you keep pressing.

On a narrow screen, scene 09's chip text shrinks with the board

Scene 09 fits its whole 480×300 board with transform: scale(parent width / 480). In the 480×300 capture window the margin around the stage draws it at about 418.6×261.6px, a scale near 0.872, so 12px chip text shows at about 10.5px. A narrower phone width shrinks the text by the same ratio, and at 340px or below the status pill and button labels drop to 10px.

A hidden tab drops the time it was away

While a tab is hidden, requestAnimationFrame pauses, and the first frame back adds only 100ms. The paper does not jump forward by several seconds at once, but any printing or fading that should have happened meanwhile is skipped.

If you want to change one constant at a time and watch where the paper gives way, unzip the download with fpfbfa6s as the password. Inside are the nine scenes' index.html and style.scss files, along with React versions that port the same math.

Line Spacing, Tear Threshold and Fade Speed Variants, Plus Accessibility for the Receipt Scenes

Name Value Feel
Wider print spacing 01 FEED_PX 14 → 30 The first-to-last line distance grows from 112px to 240px, the header moves to y −43, and visible lines drop from 9 to 7. Peak tremor stays near 2.2px.
Tougher tear threshold 02 TEAR_PX 60 → 140 The demo's 96px pull switches from falling to springing back, and teeth still holding at that moment go from 0/21 to 9/21.
Faster fade 07 FADE_RATE 0.4 → 1.6 The median time under the threshold falls from 0.86 s to 0.22 s, so the print fades about four times faster with the same stain pattern.

The controls work without a mouse or a finger. Each scene has real <button> elements (01 Print again, 02 Tear and Tug lightly, 03 Hold light, 04 Scan again and Next code, 05 Add item and Clear, 06 Pause and New roll, 07 Print again and Sunlight, 08 Tap, 09 Reset), and 03 Hold light, 06 Pause and 07 Sunlight report their on state through aria-pressed. Only the canvases in 02, 03 and 06 take keys directly with tabindex="0", and in 09 the five item chips are buttons that Tab reaches in order. On a 09 chip, Enter or Space moves it through home → A → B → C, digits 1 to 3 send it to that slot, and 0 or Backspace sends it home. preventDefault is attached only to ArrowDown, ArrowUp and Enter in 02, the left and right arrows in 03, the arrow keys in 06, and the digit and delete keys in 09, and Tab is left alone in all nine scenes.

What a screen reader hears comes from one status pill per scene marked aria-live="polite", and it changes only when something happens. Scene 01 announces twice per print, Printing and Print complete — total 11,600; scene 08 announces only a human tap, not the automatic impulses; scene 09 reads out which slot got the item and the new total.

When prefers-reduced-motion is on, scenes 01 through 08 check it at startup, skip the frame loop, and call settle() and draw() once each to leave a still frame. Scene 01 shows the fully printed slip, 04 the read state with the confirm dot lit, and 07 the text at full darkness. Input jumps straight to the result: 02 reaches its end state through finishNow() with no spring or fall, 05 sets the digits directly with shown = target, 08 changes only the status text without swinging, and 09 seats the chips in place through snap() without rolling them. The reduce block in the stylesheet does nothing more than return the cursor to its default shape.

Is a CSS Receipt Effect Possible With CSS Alone?

In this set CSS owns the stage and the buttons. .rc sizes itself with width: min(92vw, calc(160vh - 12.8vw)) and aspect-ratio: 480 / 300, and the canvases in 02 and 03 plus the chips in 09 get touch-action: none so a drag does not leak into page scrolling. The css receipt effect inside that frame is a release-time verdict (02), numbers that chase and overshoot a target (05 and 09), and a fade that runs at a different speed at each point (07); each next value depends on input and on the previous frame, which is not motion a keyframe list can hold in advance.

If a still receipt card is all you need, 9 coupon designs covers the ticket-edge side, 9 shopping cart UIs covers the whole checkout flow, and 9 CSS number counter animations isolates counting digits. Timing and drag input were written against the requestAnimationFrame and Pointer events references.

Questions Before Reusing an HTML CSS Receipt Template

Will a css receipt design dropped into a checkout show correct amounts?

The amount math exists to drive the scenes. Scene 09's slot total rounds once with round(subtotal × (1 + TAX_RATE)), and scene 05's tax row rolls without rounding at all. Replace the tax rate and the rounding rules with whatever your service requires.

Where do I change the text printed on the receipt?

Edit the ROWS array in 01, TXT_LINES in 06 and 07, and the item prices in PRICES for 05. Scene 07 draws and reads the glyph mask once at startup, so after changing the text, reload the page for the fade map to pick it up.

Can I lift a single scene out on its own?

Yes. Each scene folder runs from two files, index.html and the compiled style.css next to it, and loads no library or CDN. style.scss pulls the site's shared token file through @use, so recompiling from SCSS means bringing those token values along, and the status pill and button group are positioned against the .rc stage, so carry over the stage class and its ratio as well.

Enter the archive password

The password is inside this article. You will find it as you read.