49 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Receipt Paper Total Count-Up Button
A thermal receipt whose total counts up from zero when you press it — the number is animated by CSS, using a registered integer fed into counter-reset and printed through content. Zigzag torn edge, faded thermal ink, monospaced columns.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-49 w-full min-h-svh grid place-items-center font-tb49-mono p-[clamp(1.5rem,6vw,4rem)]">
<label class="tb-49__slip group relative cursor-pointer grid gap-1.5 w-[min(100%,17rem)] px-4.5 pt-5 pb-6.5 text-[0.8125rem] leading-relaxed tabular-nums text-tb49-ink drop-shadow-[0_12px_18px_#00000080] transition-[translate,filter] duration-300 ease-tb49-out [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 hover:drop-shadow-[0_18px_24px_#0000008c] has-focus-visible:-translate-y-0.5 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-tb49-accent active:translate-y-px motion-reduce:transition-none">
<input type="checkbox" aria-label="Total the order" class="sr-only">
<span class="text-center font-bold tracking-[0.18em] uppercase text-xs">Corner Roastery</span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Filter, 12oz</span><span>18.00</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Beans, 250g</span><span>22.60</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Service</span><span>8.00</span></span>
<span aria-hidden="true" class="block h-0 my-1.5 border-t border-dashed border-tb49-rule group-has-checked:border-solid"></span>
<span class="flex items-baseline justify-between gap-4">
<span class="font-bold tracking-[0.12em] uppercase text-xs">Total</span>
<span class="relative text-2xl font-bold tracking-[-0.02em] text-tb49-accent">
<span aria-hidden="true" class="tb-49__total hidden group-has-checked:animate-tb49-count"></span>
<span class="tb-49__fallback inline">$48.60</span>
</span>
</span>
<span class="text-center text-[0.6875rem] tracking-[0.06em] text-tb49-muted">Tap to tally · thank you</span>
</label>
</div><div class="tb-49 w-full min-h-svh grid place-items-center font-tb49-mono p-[clamp(1.5rem,6vw,4rem)]">
<label class="tb-49__slip group relative cursor-pointer grid gap-1.5 w-[min(100%,17rem)] px-4.5 pt-5 pb-6.5 text-[0.8125rem] leading-relaxed tabular-nums text-tb49-ink drop-shadow-[0_12px_18px_#00000080] transition-[translate,filter] duration-300 ease-tb49-out [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 hover:drop-shadow-[0_18px_24px_#0000008c] has-focus-visible:-translate-y-0.5 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-tb49-accent active:translate-y-px motion-reduce:transition-none">
<input type="checkbox" aria-label="Total the order" class="sr-only">
<span class="text-center font-bold tracking-[0.18em] uppercase text-xs">Corner Roastery</span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Filter, 12oz</span><span>18.00</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Beans, 250g</span><span>22.60</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Service</span><span>8.00</span></span>
<span aria-hidden="true" class="block h-0 my-1.5 border-t border-dashed border-tb49-rule group-has-checked:border-solid"></span>
<span class="flex items-baseline justify-between gap-4">
<span class="font-bold tracking-[0.12em] uppercase text-xs">Total</span>
<span class="relative text-2xl font-bold tracking-[-0.02em] text-tb49-accent">
<span aria-hidden="true" class="tb-49__total hidden group-has-checked:animate-tb49-count"></span>
<span class="tb-49__fallback inline">$48.60</span>
</span>
</span>
<span class="text-center text-[0.6875rem] tracking-[0.06em] text-tb49-muted">Tap to tally · thank you</span>
</label>
</div>@import "tailwindcss";
@property --tb-49-n {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@theme {
--color-tb49-bg: #26262a;
--color-tb49-bg: oklch(0.28 0.006 285);
--color-tb49-paper: #f7f4ec;
--color-tb49-paper: oklch(0.97 0.012 90);
--color-tb49-paper-lo: #e7e2d5;
--color-tb49-paper-lo: oklch(0.91 0.014 88);
--color-tb49-ink: #2c2a26;
--color-tb49-ink: oklch(0.27 0.012 85);
--color-tb49-muted: #5f5a50;
--color-tb49-muted: oklch(0.46 0.014 85);
--color-tb49-rule: #2c2a2659;
--color-tb49-rule: oklch(0.27 0.012 85 / 0.35);
--color-tb49-accent: #1f6f5c;
--color-tb49-accent: oklch(0.45 0.1 168);
--font-tb49-mono: "Courier Prime", ui-monospace, "Courier New", Courier, monospace;
--ease-tb49-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb49-count: tb-49-count 1.5s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
@keyframes tb-49-count {
from {
--tb-49-n: 0;
}
to {
--tb-49-n: 4860;
}
}
.tb-49 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 20rem at 50% 34%, rgba(255,255,255,.05), transparent 72%), var(--color-tb49-bg);
}
/* THERMAL STOCK + ZIGZAG TEAR: a .4rem strip of downward teeth at the bottom,
unioned with a base covering the rest. */
.tb-49__slip {
background-color: var(--color-tb49-paper);
background-image: radial-gradient(.05rem .05rem at 30% 40%, rgba(90,84,70,.14) 99%, transparent), linear-gradient(180deg, var(--color-tb49-paper), var(--color-tb49-paper-lo));
background-size: 5px 5px, 100% 100%;
-webkit-mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
}
/* THE COUNT-UP ENGINE. Registration + counter split + generated content: none of
this has a utility form, and the real-text fallback below is why it is safe. */
@supports (counter-reset: x var(--tb-49-n)) {
.tb-49__total {
display: inline;
counter-reset: tb-49-a calc(var(--tb-49-n) / 100) tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100);
}
.tb-49__total::after {
content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero);
}
.tb-49__fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tb-49__total {
animation: none !important;
--tb-49-n: 4860;
}
}
@media (prefers-color-scheme: light) {
.tb-49:not([data-theme="dark"]) {
--color-tb49-bg: oklch(0.33 0.006 285);
}
}
.tb-49[data-theme="light"] {
--color-tb49-bg: oklch(0.33 0.006 285);
}
@media (forced-colors: active) {
.tb-49__slip {
background: Canvas;
color: CanvasText;
border: 1px solid CanvasText;
mask: none;
-webkit-mask: none;
filter: none;
}
.tb-49__slip span {
color: CanvasText;
}
}@import "tailwindcss";
@property --tb-49-n {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@theme {
--color-tb49-bg: #26262a;
--color-tb49-bg: oklch(0.28 0.006 285);
--color-tb49-paper: #f7f4ec;
--color-tb49-paper: oklch(0.97 0.012 90);
--color-tb49-paper-lo: #e7e2d5;
--color-tb49-paper-lo: oklch(0.91 0.014 88);
--color-tb49-ink: #2c2a26;
--color-tb49-ink: oklch(0.27 0.012 85);
--color-tb49-muted: #5f5a50;
--color-tb49-muted: oklch(0.46 0.014 85);
--color-tb49-rule: #2c2a2659;
--color-tb49-rule: oklch(0.27 0.012 85 / 0.35);
--color-tb49-accent: #1f6f5c;
--color-tb49-accent: oklch(0.45 0.1 168);
--font-tb49-mono: "Courier Prime", ui-monospace, "Courier New", Courier, monospace;
--ease-tb49-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb49-count: tb-49-count 1.5s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
@keyframes tb-49-count {
from {
--tb-49-n: 0;
}
to {
--tb-49-n: 4860;
}
}
.tb-49 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 20rem at 50% 34%, rgba(255,255,255,.05), transparent 72%), var(--color-tb49-bg);
}
/* THERMAL STOCK + ZIGZAG TEAR: a .4rem strip of downward teeth at the bottom,
unioned with a base covering the rest. */
.tb-49__slip {
background-color: var(--color-tb49-paper);
background-image: radial-gradient(.05rem .05rem at 30% 40%, rgba(90,84,70,.14) 99%, transparent), linear-gradient(180deg, var(--color-tb49-paper), var(--color-tb49-paper-lo));
background-size: 5px 5px, 100% 100%;
-webkit-mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
}
/* THE COUNT-UP ENGINE. Registration + counter split + generated content: none of
this has a utility form, and the real-text fallback below is why it is safe. */
@supports (counter-reset: x var(--tb-49-n)) {
.tb-49__total {
display: inline;
counter-reset: tb-49-a calc(var(--tb-49-n) / 100) tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100);
}
.tb-49__total::after {
content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero);
}
.tb-49__fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tb-49__total {
animation: none !important;
--tb-49-n: 4860;
}
}
@media (prefers-color-scheme: light) {
.tb-49:not([data-theme="dark"]) {
--color-tb49-bg: oklch(0.33 0.006 285);
}
}
.tb-49[data-theme="light"] {
--color-tb49-bg: oklch(0.33 0.006 285);
}
@media (forced-colors: active) {
.tb-49__slip {
background: Canvas;
color: CanvasText;
border: 1px solid CanvasText;
mask: none;
-webkit-mask: none;
filter: none;
}
.tb-49__slip span {
color: CanvasText;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@property --tb-49-n {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@theme {
--color-tb49-bg: #26262a;
--color-tb49-bg: oklch(0.28 0.006 285);
--color-tb49-paper: #f7f4ec;
--color-tb49-paper: oklch(0.97 0.012 90);
--color-tb49-paper-lo: #e7e2d5;
--color-tb49-paper-lo: oklch(0.91 0.014 88);
--color-tb49-ink: #2c2a26;
--color-tb49-ink: oklch(0.27 0.012 85);
--color-tb49-muted: #5f5a50;
--color-tb49-muted: oklch(0.46 0.014 85);
--color-tb49-rule: #2c2a2659;
--color-tb49-rule: oklch(0.27 0.012 85 / 0.35);
--color-tb49-accent: #1f6f5c;
--color-tb49-accent: oklch(0.45 0.1 168);
--font-tb49-mono: "Courier Prime", ui-monospace, "Courier New", Courier, monospace;
--ease-tb49-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb49-count: tb-49-count 1.5s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
@keyframes tb-49-count {
from {
--tb-49-n: 0;
}
to {
--tb-49-n: 4860;
}
}
.tb-49 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 20rem at 50% 34%, rgba(255,255,255,.05), transparent 72%), var(--color-tb49-bg);
}
/* THERMAL STOCK + ZIGZAG TEAR: a .4rem strip of downward teeth at the bottom,
unioned with a base covering the rest. */
.tb-49__slip {
background-color: var(--color-tb49-paper);
background-image: radial-gradient(.05rem .05rem at 30% 40%, rgba(90,84,70,.14) 99%, transparent), linear-gradient(180deg, var(--color-tb49-paper), var(--color-tb49-paper-lo));
background-size: 5px 5px, 100% 100%;
-webkit-mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
}
/* THE COUNT-UP ENGINE. Registration + counter split + generated content: none of
this has a utility form, and the real-text fallback below is why it is safe. */
@supports (counter-reset: x var(--tb-49-n)) {
.tb-49__total {
display: inline;
counter-reset: tb-49-a calc(var(--tb-49-n) / 100) tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100);
}
.tb-49__total::after {
content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero);
}
.tb-49__fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tb-49__total {
animation: none !important;
--tb-49-n: 4860;
}
}
@media (prefers-color-scheme: light) {
.tb-49:not([data-theme="dark"]) {
--color-tb49-bg: oklch(0.33 0.006 285);
}
}
.tb-49[data-theme="light"] {
--color-tb49-bg: oklch(0.33 0.006 285);
}
@media (forced-colors: active) {
.tb-49__slip {
background: Canvas;
color: CanvasText;
border: 1px solid CanvasText;
mask: none;
-webkit-mask: none;
filter: none;
}
.tb-49__slip span {
color: CanvasText;
}
}@import "tailwindcss";
@property --tb-49-n {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@theme {
--color-tb49-bg: #26262a;
--color-tb49-bg: oklch(0.28 0.006 285);
--color-tb49-paper: #f7f4ec;
--color-tb49-paper: oklch(0.97 0.012 90);
--color-tb49-paper-lo: #e7e2d5;
--color-tb49-paper-lo: oklch(0.91 0.014 88);
--color-tb49-ink: #2c2a26;
--color-tb49-ink: oklch(0.27 0.012 85);
--color-tb49-muted: #5f5a50;
--color-tb49-muted: oklch(0.46 0.014 85);
--color-tb49-rule: #2c2a2659;
--color-tb49-rule: oklch(0.27 0.012 85 / 0.35);
--color-tb49-accent: #1f6f5c;
--color-tb49-accent: oklch(0.45 0.1 168);
--font-tb49-mono: "Courier Prime", ui-monospace, "Courier New", Courier, monospace;
--ease-tb49-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb49-count: tb-49-count 1.5s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
@keyframes tb-49-count {
from {
--tb-49-n: 0;
}
to {
--tb-49-n: 4860;
}
}
.tb-49 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 20rem at 50% 34%, rgba(255,255,255,.05), transparent 72%), var(--color-tb49-bg);
}
/* THERMAL STOCK + ZIGZAG TEAR: a .4rem strip of downward teeth at the bottom,
unioned with a base covering the rest. */
.tb-49__slip {
background-color: var(--color-tb49-paper);
background-image: radial-gradient(.05rem .05rem at 30% 40%, rgba(90,84,70,.14) 99%, transparent), linear-gradient(180deg, var(--color-tb49-paper), var(--color-tb49-paper-lo));
background-size: 5px 5px, 100% 100%;
-webkit-mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
}
/* THE COUNT-UP ENGINE. Registration + counter split + generated content: none of
this has a utility form, and the real-text fallback below is why it is safe. */
@supports (counter-reset: x var(--tb-49-n)) {
.tb-49__total {
display: inline;
counter-reset: tb-49-a calc(var(--tb-49-n) / 100) tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100);
}
.tb-49__total::after {
content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero);
}
.tb-49__fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tb-49__total {
animation: none !important;
--tb-49-n: 4860;
}
}
@media (prefers-color-scheme: light) {
.tb-49:not([data-theme="dark"]) {
--color-tb49-bg: oklch(0.33 0.006 285);
}
}
.tb-49[data-theme="light"] {
--color-tb49-bg: oklch(0.33 0.006 285);
}
@media (forced-colors: active) {
.tb-49__slip {
background: Canvas;
color: CanvasText;
border: 1px solid CanvasText;
mask: none;
-webkit-mask: none;
filter: none;
}
.tb-49__slip span {
color: CanvasText;
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: the receipt's whole layout, the dashed-to-solid rule on
group-has-checked:, the lift, the focus path, and the animation trigger. CSS: the@propertyregistration, the keyframe, the counter split with its generated content, the@supportsfallback switch, and the zigzag mask. - This is the most CSS-dependent demo in the collection, and it is the one where that is least negotiable. Every piece of the count-up is a registration, a counter declaration or a
contentstring — three things Tailwind deliberately has no utilities for, and should not. - What the port does add is a clean trigger:
group-has-checked:animate-tb49-countreplaces.btn:has(input:checked) .total { animation: ... }, and the animation itself is an@themetoken, so its duration and curve are tunable at the token layer. - A11y fix from the CSS original:
--muted:#7c776con the thermal paper is ≈3.4:1, and it carries every line item — the actual contents of the receipt. The port darkens it tooklch(0.46 0.014 85)(≈5.6:1). The faded-thermal look survives; the prices are now readable.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Receipt Paper Total Count-Up Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-receipt-paper-total-count-up-button/
A thermal receipt whose total counts up from zero when you press it — the number is animated by CSS, using a registered integer fed into counter-reset and printed through content. Zigzag torn edge, faded thermal ink, monospaced columns.
## HTML
```html
<div class="tb-49 w-full min-h-svh grid place-items-center font-tb49-mono p-[clamp(1.5rem,6vw,4rem)]">
<label class="tb-49__slip group relative cursor-pointer grid gap-1.5 w-[min(100%,17rem)] px-4.5 pt-5 pb-6.5 text-[0.8125rem] leading-relaxed tabular-nums text-tb49-ink drop-shadow-[0_12px_18px_#00000080] transition-[translate,filter] duration-300 ease-tb49-out [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 hover:drop-shadow-[0_18px_24px_#0000008c] has-focus-visible:-translate-y-0.5 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-tb49-accent active:translate-y-px motion-reduce:transition-none">
<input type="checkbox" aria-label="Total the order" class="sr-only">
<span class="text-center font-bold tracking-[0.18em] uppercase text-xs">Corner Roastery</span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Filter, 12oz</span><span>18.00</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Beans, 250g</span><span>22.60</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Service</span><span>8.00</span></span>
<span aria-hidden="true" class="block h-0 my-1.5 border-t border-dashed border-tb49-rule group-has-checked:border-solid"></span>
<span class="flex items-baseline justify-between gap-4">
<span class="font-bold tracking-[0.12em] uppercase text-xs">Total</span>
<span class="relative text-2xl font-bold tracking-[-0.02em] text-tb49-accent">
<span aria-hidden="true" class="tb-49__total hidden group-has-checked:animate-tb49-count"></span>
<span class="tb-49__fallback inline">$48.60</span>
</span>
</span>
<span class="text-center text-[0.6875rem] tracking-[0.06em] text-tb49-muted">Tap to tally · thank you</span>
</label>
</div>
```
## CSS
```css
@import "tailwindcss";
@property --tb-49-n {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@theme {
--color-tb49-bg: #26262a;
--color-tb49-bg: oklch(0.28 0.006 285);
--color-tb49-paper: #f7f4ec;
--color-tb49-paper: oklch(0.97 0.012 90);
--color-tb49-paper-lo: #e7e2d5;
--color-tb49-paper-lo: oklch(0.91 0.014 88);
--color-tb49-ink: #2c2a26;
--color-tb49-ink: oklch(0.27 0.012 85);
--color-tb49-muted: #5f5a50;
--color-tb49-muted: oklch(0.46 0.014 85);
--color-tb49-rule: #2c2a2659;
--color-tb49-rule: oklch(0.27 0.012 85 / 0.35);
--color-tb49-accent: #1f6f5c;
--color-tb49-accent: oklch(0.45 0.1 168);
--font-tb49-mono: "Courier Prime", ui-monospace, "Courier New", Courier, monospace;
--ease-tb49-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb49-count: tb-49-count 1.5s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
@keyframes tb-49-count {
from {
--tb-49-n: 0;
}
to {
--tb-49-n: 4860;
}
}
.tb-49 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 20rem at 50% 34%, rgba(255,255,255,.05), transparent 72%), var(--color-tb49-bg);
}
/* THERMAL STOCK + ZIGZAG TEAR: a .4rem strip of downward teeth at the bottom,
unioned with a base covering the rest. */
.tb-49__slip {
background-color: var(--color-tb49-paper);
background-image: radial-gradient(.05rem .05rem at 30% 40%, rgba(90,84,70,.14) 99%, transparent), linear-gradient(180deg, var(--color-tb49-paper), var(--color-tb49-paper-lo));
background-size: 5px 5px, 100% 100%;
-webkit-mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
}
/* THE COUNT-UP ENGINE. Registration + counter split + generated content: none of
this has a utility form, and the real-text fallback below is why it is safe. */
@supports (counter-reset: x var(--tb-49-n)) {
.tb-49__total {
display: inline;
counter-reset: tb-49-a calc(var(--tb-49-n) / 100) tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100);
}
.tb-49__total::after {
content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero);
}
.tb-49__fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tb-49__total {
animation: none !important;
--tb-49-n: 4860;
}
}
@media (prefers-color-scheme: light) {
.tb-49:not([data-theme="dark"]) {
--color-tb49-bg: oklch(0.33 0.006 285);
}
}
.tb-49[data-theme="light"] {
--color-tb49-bg: oklch(0.33 0.006 285);
}
@media (forced-colors: active) {
.tb-49__slip {
background: Canvas;
color: CanvasText;
border: 1px solid CanvasText;
mask: none;
-webkit-mask: none;
filter: none;
}
.tb-49__slip span {
color: CanvasText;
}
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Receipt Paper Total Count-Up Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-receipt-paper-total-count-up-button/
A thermal receipt whose total counts up from zero when you press it — the number is animated by CSS, using a registered integer fed into counter-reset and printed through content. Zigzag torn edge, faded thermal ink, monospaced columns.
## HTML
```html
<div class="tb-49 w-full min-h-svh grid place-items-center font-tb49-mono p-[clamp(1.5rem,6vw,4rem)]">
<label class="tb-49__slip group relative cursor-pointer grid gap-1.5 w-[min(100%,17rem)] px-4.5 pt-5 pb-6.5 text-[0.8125rem] leading-relaxed tabular-nums text-tb49-ink drop-shadow-[0_12px_18px_#00000080] transition-[translate,filter] duration-300 ease-tb49-out [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 hover:drop-shadow-[0_18px_24px_#0000008c] has-focus-visible:-translate-y-0.5 has-focus-visible:outline-2 has-focus-visible:outline-offset-4 has-focus-visible:outline-tb49-accent active:translate-y-px motion-reduce:transition-none">
<input type="checkbox" aria-label="Total the order" class="sr-only">
<span class="text-center font-bold tracking-[0.18em] uppercase text-xs">Corner Roastery</span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Filter, 12oz</span><span>18.00</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Beans, 250g</span><span>22.60</span></span>
<span class="flex justify-between gap-4 text-tb49-muted"><span>Service</span><span>8.00</span></span>
<span aria-hidden="true" class="block h-0 my-1.5 border-t border-dashed border-tb49-rule group-has-checked:border-solid"></span>
<span class="flex items-baseline justify-between gap-4">
<span class="font-bold tracking-[0.12em] uppercase text-xs">Total</span>
<span class="relative text-2xl font-bold tracking-[-0.02em] text-tb49-accent">
<span aria-hidden="true" class="tb-49__total hidden group-has-checked:animate-tb49-count"></span>
<span class="tb-49__fallback inline">$48.60</span>
</span>
</span>
<span class="text-center text-[0.6875rem] tracking-[0.06em] text-tb49-muted">Tap to tally · thank you</span>
</label>
</div>
```
## CSS
```css
@import "tailwindcss";
@property --tb-49-n {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@theme {
--color-tb49-bg: #26262a;
--color-tb49-bg: oklch(0.28 0.006 285);
--color-tb49-paper: #f7f4ec;
--color-tb49-paper: oklch(0.97 0.012 90);
--color-tb49-paper-lo: #e7e2d5;
--color-tb49-paper-lo: oklch(0.91 0.014 88);
--color-tb49-ink: #2c2a26;
--color-tb49-ink: oklch(0.27 0.012 85);
--color-tb49-muted: #5f5a50;
--color-tb49-muted: oklch(0.46 0.014 85);
--color-tb49-rule: #2c2a2659;
--color-tb49-rule: oklch(0.27 0.012 85 / 0.35);
--color-tb49-accent: #1f6f5c;
--color-tb49-accent: oklch(0.45 0.1 168);
--font-tb49-mono: "Courier Prime", ui-monospace, "Courier New", Courier, monospace;
--ease-tb49-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb49-count: tb-49-count 1.5s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
}
@keyframes tb-49-count {
from {
--tb-49-n: 0;
}
to {
--tb-49-n: 4860;
}
}
.tb-49 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 20rem at 50% 34%, rgba(255,255,255,.05), transparent 72%), var(--color-tb49-bg);
}
/* THERMAL STOCK + ZIGZAG TEAR: a .4rem strip of downward teeth at the bottom,
unioned with a base covering the rest. */
.tb-49__slip {
background-color: var(--color-tb49-paper);
background-image: radial-gradient(.05rem .05rem at 30% 40%, rgba(90,84,70,.14) 99%, transparent), linear-gradient(180deg, var(--color-tb49-paper), var(--color-tb49-paper-lo));
background-size: 5px 5px, 100% 100%;
-webkit-mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
mask: conic-gradient(from 135deg at 50% 0, #000 90deg, transparent 0) 0 100%/.76rem .4rem repeat-x, linear-gradient(#000, #000) 0 0/100% calc(100% - .38rem) no-repeat;
}
/* THE COUNT-UP ENGINE. Registration + counter split + generated content: none of
this has a utility form, and the real-text fallback below is why it is safe. */
@supports (counter-reset: x var(--tb-49-n)) {
.tb-49__total {
display: inline;
counter-reset: tb-49-a calc(var(--tb-49-n) / 100) tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100);
}
.tb-49__total::after {
content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero);
}
.tb-49__fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tb-49__total {
animation: none !important;
--tb-49-n: 4860;
}
}
@media (prefers-color-scheme: light) {
.tb-49:not([data-theme="dark"]) {
--color-tb49-bg: oklch(0.33 0.006 285);
}
}
.tb-49[data-theme="light"] {
--color-tb49-bg: oklch(0.33 0.006 285);
}
@media (forced-colors: active) {
.tb-49__slip {
background: Canvas;
color: CanvasText;
border: 1px solid CanvasText;
mask: none;
-webkit-mask: none;
filter: none;
}
.tb-49__slip span {
color: CanvasText;
}
}
```How this works
Three features chained give a JavaScript-free count-up: @property makes an integer animatable, counter-reset accepts that integer, and a counter can be printed in generated content.
@property --tb-49-n { syntax: '<integer>'; inherits: false; initial-value: 0; }
.tb-49__total { counter-reset: tb-49-a calc(var(--tb-49-n) / 100)
tb-49-b calc(var(--tb-49-n) - (var(--tb-49-n) / 100) * 100); }
.tb-49__total::after { content: "$" counter(tb-49-a) "." counter(tb-49-b, decimal-leading-zero); }Cents are handled by counting hundredths — integers only, never floats.
The fallback is real text, not a placeholder. Firefox shows $48.60 as an ordinary span; where animated counters work, @supports hides it and shows the counter instead. Nothing important lives only in generated content.
Tailwind's contribution is the trigger and the chrome — the animation token, the has-checked: variant, the layout, the paper. The engine stays in CSS because every piece of it is a registration or a generated-content rule.
Make it yours
- Target value is the keyframe's end; keep it in hundredths if you display cents.
- A slow-out curve reads as a machine tallying; linear reads as a stopwatch.
- Tear pitch is the mask's tile width.
- Add line items by copying a row — the grid is two columns with
tabular-nums. --color-tb49-accentis the only colour on the receipt. Keep it that way.
Gotchas — read before shipping
- Animated counters are Chrome/Safari-only today. Always leave the final value as real text, or the total reads as $0 in Firefox.
- A number animated in
contentis not reliably announced. Real text carries the value. - Integer animation rounds per frame — count hundredths, never floats.
- Keep the counter name demo-scoped;
counter-resetre-declares every frame. - Never put important information only in
::after. Some assistive tech ignores generated content entirely.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property matches Tailwind v4's own floor. Animated counter printing works in Chrome and Safari; Firefox shows the static fallback total, which is real text in the DOM.