06 / 22
All 22 in 22 Tailwind CSS Countdown Timers
- 01Accessible Tailwind CSS Countdown Timer with ARIA Live Region
- 02Pure Tailwind CSS Countdown Timer Without JavaScript
- 03Tailwind Session Timeout Warning Modal with Countdown
- 04Tailwind Circular Countdown Timer with SVG Progress Ring
- 05Tailwind OTP Input with Resend Code Countdown Timer
- 06Tailwind Rate Limit Button with Retry-After Countdown
- 07Tailwind Auction Countdown Timer with Anti-Snipe Extension
- 08Tailwind Delivery ETA Countdown with Order Tracking Steps
- 09Tailwind Waitlist Landing Page with Launch Countdown
- 10Tailwind Webinar Countdown Timer with Time Zone Display
- 11Tailwind Appointment Reminder Card with Countdown
- 12Tailwind Exam Timer with Time Remaining Warnings
- 13Tailwind Free Trial Countdown Banner for SaaS Apps
- 14Tailwind Scheduled Maintenance Countdown Status Page
- 15Tailwind Virtual Waiting Room Queue Countdown
- 16Tailwind Daily Streak Countdown Widget for Habit Apps
- 17Tailwind Flash Sale Countdown Timer for Product Pages
- 18Tailwind Sticky Announcement Bar with Countdown Timer
- 19Tailwind Shopping Cart Reservation Countdown Timer
- 20Tailwind Coming Soon Page with Launch Countdown Timer
- 21Tailwind Interval Workout Timer with Circular Progress
- 22Tailwind Retro Flip Clock Countdown Timer
Tailwind Rate Limit Button with Retry-After Countdown
What a 429 Too Many Requests should look like in a UI: the button enters a cooldown derived from the Retry-After header and an amber fill drains across its label as the seconds tick away, with the response headers beside it so the number is traceable. New: a five-pip quota meter, a scrolling request log and a status-coloured header table, all from data-* variants.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcd-06 w-full min-h-svh grid place-items-center bg-tcd06-page bg-[radial-gradient(50%_40%_at_50%_30%,var(--color-tcd06-glow),transparent_70%)] px-3 py-12 font-tcd06-mono text-tcd06-ink" aria-labelledby="tcd-06-title">
<div class="w-full max-w-[640px] overflow-hidden rounded-xl border border-tcd06-rule bg-tcd06-panel shadow-[0_40px_80px_-40px_rgb(0_0_0/0.9)]">
<header class="flex min-w-0 items-center gap-2.5 border-b border-tcd06-rule px-4 py-3">
<span aria-hidden="true" class="flex gap-1.5"><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span></span>
<h2 id="tcd-06-title" class="ml-1 min-w-0 flex-1 text-[12.5px] font-medium tracking-[-0.01em] [overflow-wrap:anywhere]"><span class="text-tcd06-acc">POST</span> /v2/exports</h2>
<span class="flex-none text-[10.5px] text-tcd06-mut">talon-api · staging</span>
</header>
<div class="flex flex-wrap items-center gap-3 border-b border-tcd06-rule p-4">
<button type="button" data-go class="min-h-11 min-w-[168px] cursor-pointer rounded-md border border-tcd06-rule bg-[linear-gradient(var(--color-tcd06-acc-a),var(--color-tcd06-acc-a))] bg-[length:var(--fill,0%)_100%] bg-no-repeat px-4 text-[12.5px] font-semibold tabular-nums transition-[background-size,border-color,color] duration-1000 ease-linear enabled:hover:border-tcd06-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tcd06-acc disabled:cursor-not-allowed disabled:border-tcd06-acc/45 disabled:text-tcd06-acc motion-reduce:transition-none"><span data-label>Run request</span></button>
<div class="grid min-w-0 flex-[1_1_180px] gap-1.5">
<p role="timer" data-state class="text-[11.5px] text-tcd06-mut data-limited:text-tcd06-acc">Ready · 5 requests per minute</p>
<div aria-hidden="true" data-pips class="flex gap-1">
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
</div>
</div>
</div>
<dl class="grid grid-cols-1 border-b border-tcd06-rule sm:grid-cols-2">
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r"><dt class="text-tcd06-mut">status</dt><dd data-status class="font-semibold data-[status=202]:text-tcd06-ok data-[status=429]:text-tcd06-danger">—</dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">retry-after</dt><dd class="font-semibold"><time datetime="" data-retry>—</time></dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r sm:border-b-0"><dt class="text-tcd06-mut">x-ratelimit-remaining</dt><dd data-left class="font-semibold">5</dd></div>
<div class="flex justify-between gap-3 px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">x-ratelimit-reset</dt><dd data-reset class="font-semibold">—</dd></div>
</dl>
<ol data-log aria-label="Request log" class="grid max-h-40 gap-1 overflow-auto bg-tcd06-page/60 px-4 py-3 text-[11.5px] leading-relaxed text-tcd06-mut">
<li>$ waiting for first run</li>
</ol>
<output data-live aria-live="polite" class="sr-only"></output>
<p class="border-t border-tcd06-rule px-4 py-3 text-[10.5px] leading-relaxed text-tcd06-mut text-pretty">Retry-After may arrive as delta-seconds or an HTTP-date. Both are parsed; a missing header falls back to a fixed 15s wait.</p>
</div>
</section><section class="tcd-06 w-full min-h-svh grid place-items-center bg-tcd06-page bg-[radial-gradient(50%_40%_at_50%_30%,var(--color-tcd06-glow),transparent_70%)] px-3 py-12 font-tcd06-mono text-tcd06-ink" aria-labelledby="tcd-06-title">
<div class="w-full max-w-[640px] overflow-hidden rounded-xl border border-tcd06-rule bg-tcd06-panel shadow-[0_40px_80px_-40px_rgb(0_0_0/0.9)]">
<header class="flex min-w-0 items-center gap-2.5 border-b border-tcd06-rule px-4 py-3">
<span aria-hidden="true" class="flex gap-1.5"><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span></span>
<h2 id="tcd-06-title" class="ml-1 min-w-0 flex-1 text-[12.5px] font-medium tracking-[-0.01em] [overflow-wrap:anywhere]"><span class="text-tcd06-acc">POST</span> /v2/exports</h2>
<span class="flex-none text-[10.5px] text-tcd06-mut">talon-api · staging</span>
</header>
<div class="flex flex-wrap items-center gap-3 border-b border-tcd06-rule p-4">
<button type="button" data-go class="min-h-11 min-w-[168px] cursor-pointer rounded-md border border-tcd06-rule bg-[linear-gradient(var(--color-tcd06-acc-a),var(--color-tcd06-acc-a))] bg-[length:var(--fill,0%)_100%] bg-no-repeat px-4 text-[12.5px] font-semibold tabular-nums transition-[background-size,border-color,color] duration-1000 ease-linear enabled:hover:border-tcd06-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tcd06-acc disabled:cursor-not-allowed disabled:border-tcd06-acc/45 disabled:text-tcd06-acc motion-reduce:transition-none"><span data-label>Run request</span></button>
<div class="grid min-w-0 flex-[1_1_180px] gap-1.5">
<p role="timer" data-state class="text-[11.5px] text-tcd06-mut data-limited:text-tcd06-acc">Ready · 5 requests per minute</p>
<div aria-hidden="true" data-pips class="flex gap-1">
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
</div>
</div>
</div>
<dl class="grid grid-cols-1 border-b border-tcd06-rule sm:grid-cols-2">
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r"><dt class="text-tcd06-mut">status</dt><dd data-status class="font-semibold data-[status=202]:text-tcd06-ok data-[status=429]:text-tcd06-danger">—</dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">retry-after</dt><dd class="font-semibold"><time datetime="" data-retry>—</time></dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r sm:border-b-0"><dt class="text-tcd06-mut">x-ratelimit-remaining</dt><dd data-left class="font-semibold">5</dd></div>
<div class="flex justify-between gap-3 px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">x-ratelimit-reset</dt><dd data-reset class="font-semibold">—</dd></div>
</dl>
<ol data-log aria-label="Request log" class="grid max-h-40 gap-1 overflow-auto bg-tcd06-page/60 px-4 py-3 text-[11.5px] leading-relaxed text-tcd06-mut">
<li>$ waiting for first run</li>
</ol>
<output data-live aria-live="polite" class="sr-only"></output>
<p class="border-t border-tcd06-rule px-4 py-3 text-[10.5px] leading-relaxed text-tcd06-mut text-pretty">Retry-After may arrive as delta-seconds or an HTTP-date. Both are parsed; a missing header falls back to a fixed 15s wait.</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcd06-page: #000000;
--color-tcd06-page: oklch(0 0 0);
--color-tcd06-glow: #14110a;
--color-tcd06-glow: oklch(0.2 0.03 78);
--color-tcd06-panel: #0c0c0e;
--color-tcd06-panel: oklch(0.16 0.004 280);
--color-tcd06-ink: #f4f4f5;
--color-tcd06-ink: oklch(0.96 0.003 280);
--color-tcd06-mut: #9a9aa3;
--color-tcd06-mut: oklch(0.7 0.01 280);
--color-tcd06-rule: #232328;
--color-tcd06-rule: oklch(0.27 0.006 280);
--color-tcd06-acc: #ffb224;
--color-tcd06-acc: oklch(0.81 0.16 78);
--color-tcd06-acc-a: rgb(255 178 36 / 0.22);
--color-tcd06-danger: #ff5c5c;
--color-tcd06-danger: oklch(0.69 0.19 25);
--color-tcd06-ok: #4ade80;
--color-tcd06-ok: oklch(0.82 0.17 148);
--font-tcd06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tcd-06 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tcd06-page: #000000;
--color-tcd06-page: oklch(0 0 0);
--color-tcd06-glow: #14110a;
--color-tcd06-glow: oklch(0.2 0.03 78);
--color-tcd06-panel: #0c0c0e;
--color-tcd06-panel: oklch(0.16 0.004 280);
--color-tcd06-ink: #f4f4f5;
--color-tcd06-ink: oklch(0.96 0.003 280);
--color-tcd06-mut: #9a9aa3;
--color-tcd06-mut: oklch(0.7 0.01 280);
--color-tcd06-rule: #232328;
--color-tcd06-rule: oklch(0.27 0.006 280);
--color-tcd06-acc: #ffb224;
--color-tcd06-acc: oklch(0.81 0.16 78);
--color-tcd06-acc-a: rgb(255 178 36 / 0.22);
--color-tcd06-danger: #ff5c5c;
--color-tcd06-danger: oklch(0.69 0.19 25);
--color-tcd06-ok: #4ade80;
--color-tcd06-ok: oklch(0.82 0.17 148);
--font-tcd06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tcd-06 {
width: 100%;
min-height: 100svh;
}(() => {
const root = document.querySelector('.tcd-06');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const q = (s) => root.querySelector(s);
const go = q('[data-go]'), label = q('[data-label]'), state = q('[data-state]'), log = q('[data-log]');
const live = q('[data-live]'), pips = [...q('[data-pips]').children], status = q('[data-status]');
const LIMIT = 5, FALLBACK = 15;
let left = LIMIT, id = 0, total = 0, target = 0;
const stamp = () => new Date().toLocaleTimeString([], { hour12: false });
const line = (t, cls) => { const li = document.createElement('li'); li.textContent = stamp() + ' ' + t; if (cls) li.className = cls; log.prepend(li); };
const paint = () => { q('[data-left]').textContent = left; pips.forEach((p, i) => p.toggleAttribute('data-off', i >= left)); };
const parseRetry = (v) => Number.isFinite(+v) ? +v : Number.isFinite(Date.parse(v)) ? Math.ceil((Date.parse(v) - Date.now()) / 1000) : FALLBACK;
const tick = () => {
const ms = Math.max(0, target - Date.now()), s = Math.ceil(ms / 1000);
label.textContent = 'Retry in ' + s + 's';
go.style.setProperty('--fill', (ms / total * 100).toFixed(1) + '%');
if (ms > 0) { id = setTimeout(tick, 1000 - (Date.now() % 1000)); return; }
go.disabled = false; label.textContent = 'Run request'; left = LIMIT; paint();
state.removeAttribute('data-limited'); state.textContent = 'Ready · window reset';
line('window reset · ' + LIMIT + ' requests available', 'text-tcd06-ok');
live.textContent = 'Rate limit window reset. You can run the request again.';
};
go.addEventListener('click', () => {
if (go.disabled) return;
if (left > 0) {
left -= 1; paint(); status.textContent = '202'; status.dataset.status = '202';
line('202 accepted · export queued · ' + left + ' left in window');
return;
}
const header = '9';
const secs = parseRetry(header); total = secs * 1000; target = Date.now() + total;
status.textContent = '429'; status.dataset.status = '429';
q('[data-retry]').textContent = header; q('[data-retry]').dateTime = 'PT' + secs + 'S';
q('[data-reset]').textContent = new Date(target).toLocaleTimeString([], { hour12: false });
state.setAttribute('data-limited', ''); state.textContent = 'Too Many Requests · cooling down';
line('429 too many requests · retry-after: ' + header + ' → cooldown ' + secs + 's', 'text-tcd06-danger');
go.disabled = true; go.style.setProperty('--fill', '100%');
live.textContent = 'Rate limited. Retry available in ' + secs + ' seconds.';
clearTimeout(id); requestAnimationFrame(tick);
});
root.cleanup = () => clearTimeout(id);
})();(() => {
const root = document.querySelector('.tcd-06');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const q = (s) => root.querySelector(s);
const go = q('[data-go]'), label = q('[data-label]'), state = q('[data-state]'), log = q('[data-log]');
const live = q('[data-live]'), pips = [...q('[data-pips]').children], status = q('[data-status]');
const LIMIT = 5, FALLBACK = 15;
let left = LIMIT, id = 0, total = 0, target = 0;
const stamp = () => new Date().toLocaleTimeString([], { hour12: false });
const line = (t, cls) => { const li = document.createElement('li'); li.textContent = stamp() + ' ' + t; if (cls) li.className = cls; log.prepend(li); };
const paint = () => { q('[data-left]').textContent = left; pips.forEach((p, i) => p.toggleAttribute('data-off', i >= left)); };
const parseRetry = (v) => Number.isFinite(+v) ? +v : Number.isFinite(Date.parse(v)) ? Math.ceil((Date.parse(v) - Date.now()) / 1000) : FALLBACK;
const tick = () => {
const ms = Math.max(0, target - Date.now()), s = Math.ceil(ms / 1000);
label.textContent = 'Retry in ' + s + 's';
go.style.setProperty('--fill', (ms / total * 100).toFixed(1) + '%');
if (ms > 0) { id = setTimeout(tick, 1000 - (Date.now() % 1000)); return; }
go.disabled = false; label.textContent = 'Run request'; left = LIMIT; paint();
state.removeAttribute('data-limited'); state.textContent = 'Ready · window reset';
line('window reset · ' + LIMIT + ' requests available', 'text-tcd06-ok');
live.textContent = 'Rate limit window reset. You can run the request again.';
};
go.addEventListener('click', () => {
if (go.disabled) return;
if (left > 0) {
left -= 1; paint(); status.textContent = '202'; status.dataset.status = '202';
line('202 accepted · export queued · ' + left + ' left in window');
return;
}
const header = '9';
const secs = parseRetry(header); total = secs * 1000; target = Date.now() + total;
status.textContent = '429'; status.dataset.status = '429';
q('[data-retry]').textContent = header; q('[data-retry]').dateTime = 'PT' + secs + 'S';
q('[data-reset]').textContent = new Date(target).toLocaleTimeString([], { hour12: false });
state.setAttribute('data-limited', ''); state.textContent = 'Too Many Requests · cooling down';
line('429 too many requests · retry-after: ' + header + ' → cooldown ' + secs + 's', 'text-tcd06-danger');
go.disabled = true; go.style.setProperty('--fill', '100%');
live.textContent = 'Rate limited. Retry available in ' + secs + ' seconds.';
clearTimeout(id); requestAnimationFrame(tick);
});
root.cleanup = () => clearTimeout(id);
})();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";
@theme {
--color-tcd06-page: #000000;
--color-tcd06-page: oklch(0 0 0);
--color-tcd06-glow: #14110a;
--color-tcd06-glow: oklch(0.2 0.03 78);
--color-tcd06-panel: #0c0c0e;
--color-tcd06-panel: oklch(0.16 0.004 280);
--color-tcd06-ink: #f4f4f5;
--color-tcd06-ink: oklch(0.96 0.003 280);
--color-tcd06-mut: #9a9aa3;
--color-tcd06-mut: oklch(0.7 0.01 280);
--color-tcd06-rule: #232328;
--color-tcd06-rule: oklch(0.27 0.006 280);
--color-tcd06-acc: #ffb224;
--color-tcd06-acc: oklch(0.81 0.16 78);
--color-tcd06-acc-a: rgb(255 178 36 / 0.22);
--color-tcd06-danger: #ff5c5c;
--color-tcd06-danger: oklch(0.69 0.19 25);
--color-tcd06-ok: #4ade80;
--color-tcd06-ok: oklch(0.82 0.17 148);
--font-tcd06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tcd-06 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tcd06-page: #000000;
--color-tcd06-page: oklch(0 0 0);
--color-tcd06-glow: #14110a;
--color-tcd06-glow: oklch(0.2 0.03 78);
--color-tcd06-panel: #0c0c0e;
--color-tcd06-panel: oklch(0.16 0.004 280);
--color-tcd06-ink: #f4f4f5;
--color-tcd06-ink: oklch(0.96 0.003 280);
--color-tcd06-mut: #9a9aa3;
--color-tcd06-mut: oklch(0.7 0.01 280);
--color-tcd06-rule: #232328;
--color-tcd06-rule: oklch(0.27 0.006 280);
--color-tcd06-acc: #ffb224;
--color-tcd06-acc: oklch(0.81 0.16 78);
--color-tcd06-acc-a: rgb(255 178 36 / 0.22);
--color-tcd06-danger: #ff5c5c;
--color-tcd06-danger: oklch(0.69 0.19 25);
--color-tcd06-ok: #4ade80;
--color-tcd06-ok: oklch(0.82 0.17 148);
--font-tcd06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tcd-06 {
width: 100%;
min-height: 100svh;
}<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=JetBrains+Mono:wght@400;500;600;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=JetBrains+Mono:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.cdt-06__go[data-cool]{background-size:var(--fill)}became an always-onbg-[length:var(--fill,0%)_100%]; the script sets--filland the attribute selector disappeared.[data-status="429"]colouring becamedata-[status=429]:on the cell; the disabled look becamedisabled:variants.- UI upgrade: a five-pip quota meter mirrors
x-ratelimit-remaining, so the 429 is predictable instead of a surprise on the sixth click. - UI upgrade: the single-line
<pre>became a timestamped, colour-coded request log; the headers table is a responsive two-column grid; muted text rose from0.61to0.70lightness. - Bug fix: the fill now starts at 100% and drains on the next frame, instead of jumping from 0% up to 100% before draining.
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 Rate Limit Button with Retry-After Countdown
Source: https://codefronts.com/snippets/tailwind-css-countdown-timers/tailwind-rate-limit-button-retry-after-countdown/
What a 429 Too Many Requests should look like in a UI: the button enters a cooldown derived from the Retry-After header and an amber fill drains across its label as the seconds tick away, with the response headers beside it so the number is traceable. New: a five-pip quota meter, a scrolling request log and a status-coloured header table, all from data-* variants.
## HTML
```html
<section class="tcd-06 w-full min-h-svh grid place-items-center bg-tcd06-page bg-[radial-gradient(50%_40%_at_50%_30%,var(--color-tcd06-glow),transparent_70%)] px-3 py-12 font-tcd06-mono text-tcd06-ink" aria-labelledby="tcd-06-title">
<div class="w-full max-w-[640px] overflow-hidden rounded-xl border border-tcd06-rule bg-tcd06-panel shadow-[0_40px_80px_-40px_rgb(0_0_0/0.9)]">
<header class="flex min-w-0 items-center gap-2.5 border-b border-tcd06-rule px-4 py-3">
<span aria-hidden="true" class="flex gap-1.5"><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span></span>
<h2 id="tcd-06-title" class="ml-1 min-w-0 flex-1 text-[12.5px] font-medium tracking-[-0.01em] [overflow-wrap:anywhere]"><span class="text-tcd06-acc">POST</span> /v2/exports</h2>
<span class="flex-none text-[10.5px] text-tcd06-mut">talon-api · staging</span>
</header>
<div class="flex flex-wrap items-center gap-3 border-b border-tcd06-rule p-4">
<button type="button" data-go class="min-h-11 min-w-[168px] cursor-pointer rounded-md border border-tcd06-rule bg-[linear-gradient(var(--color-tcd06-acc-a),var(--color-tcd06-acc-a))] bg-[length:var(--fill,0%)_100%] bg-no-repeat px-4 text-[12.5px] font-semibold tabular-nums transition-[background-size,border-color,color] duration-1000 ease-linear enabled:hover:border-tcd06-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tcd06-acc disabled:cursor-not-allowed disabled:border-tcd06-acc/45 disabled:text-tcd06-acc motion-reduce:transition-none"><span data-label>Run request</span></button>
<div class="grid min-w-0 flex-[1_1_180px] gap-1.5">
<p role="timer" data-state class="text-[11.5px] text-tcd06-mut data-limited:text-tcd06-acc">Ready · 5 requests per minute</p>
<div aria-hidden="true" data-pips class="flex gap-1">
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
</div>
</div>
</div>
<dl class="grid grid-cols-1 border-b border-tcd06-rule sm:grid-cols-2">
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r"><dt class="text-tcd06-mut">status</dt><dd data-status class="font-semibold data-[status=202]:text-tcd06-ok data-[status=429]:text-tcd06-danger">—</dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">retry-after</dt><dd class="font-semibold"><time datetime="" data-retry>—</time></dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r sm:border-b-0"><dt class="text-tcd06-mut">x-ratelimit-remaining</dt><dd data-left class="font-semibold">5</dd></div>
<div class="flex justify-between gap-3 px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">x-ratelimit-reset</dt><dd data-reset class="font-semibold">—</dd></div>
</dl>
<ol data-log aria-label="Request log" class="grid max-h-40 gap-1 overflow-auto bg-tcd06-page/60 px-4 py-3 text-[11.5px] leading-relaxed text-tcd06-mut">
<li>$ waiting for first run</li>
</ol>
<output data-live aria-live="polite" class="sr-only"></output>
<p class="border-t border-tcd06-rule px-4 py-3 text-[10.5px] leading-relaxed text-tcd06-mut text-pretty">Retry-After may arrive as delta-seconds or an HTTP-date. Both are parsed; a missing header falls back to a fixed 15s wait.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcd06-page: #000000;
--color-tcd06-page: oklch(0 0 0);
--color-tcd06-glow: #14110a;
--color-tcd06-glow: oklch(0.2 0.03 78);
--color-tcd06-panel: #0c0c0e;
--color-tcd06-panel: oklch(0.16 0.004 280);
--color-tcd06-ink: #f4f4f5;
--color-tcd06-ink: oklch(0.96 0.003 280);
--color-tcd06-mut: #9a9aa3;
--color-tcd06-mut: oklch(0.7 0.01 280);
--color-tcd06-rule: #232328;
--color-tcd06-rule: oklch(0.27 0.006 280);
--color-tcd06-acc: #ffb224;
--color-tcd06-acc: oklch(0.81 0.16 78);
--color-tcd06-acc-a: rgb(255 178 36 / 0.22);
--color-tcd06-danger: #ff5c5c;
--color-tcd06-danger: oklch(0.69 0.19 25);
--color-tcd06-ok: #4ade80;
--color-tcd06-ok: oklch(0.82 0.17 148);
--font-tcd06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tcd-06 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tcd-06');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const q = (s) => root.querySelector(s);
const go = q('[data-go]'), label = q('[data-label]'), state = q('[data-state]'), log = q('[data-log]');
const live = q('[data-live]'), pips = [...q('[data-pips]').children], status = q('[data-status]');
const LIMIT = 5, FALLBACK = 15;
let left = LIMIT, id = 0, total = 0, target = 0;
const stamp = () => new Date().toLocaleTimeString([], { hour12: false });
const line = (t, cls) => { const li = document.createElement('li'); li.textContent = stamp() + ' ' + t; if (cls) li.className = cls; log.prepend(li); };
const paint = () => { q('[data-left]').textContent = left; pips.forEach((p, i) => p.toggleAttribute('data-off', i >= left)); };
const parseRetry = (v) => Number.isFinite(+v) ? +v : Number.isFinite(Date.parse(v)) ? Math.ceil((Date.parse(v) - Date.now()) / 1000) : FALLBACK;
const tick = () => {
const ms = Math.max(0, target - Date.now()), s = Math.ceil(ms / 1000);
label.textContent = 'Retry in ' + s + 's';
go.style.setProperty('--fill', (ms / total * 100).toFixed(1) + '%');
if (ms > 0) { id = setTimeout(tick, 1000 - (Date.now() % 1000)); return; }
go.disabled = false; label.textContent = 'Run request'; left = LIMIT; paint();
state.removeAttribute('data-limited'); state.textContent = 'Ready · window reset';
line('window reset · ' + LIMIT + ' requests available', 'text-tcd06-ok');
live.textContent = 'Rate limit window reset. You can run the request again.';
};
go.addEventListener('click', () => {
if (go.disabled) return;
if (left > 0) {
left -= 1; paint(); status.textContent = '202'; status.dataset.status = '202';
line('202 accepted · export queued · ' + left + ' left in window');
return;
}
const header = '9';
const secs = parseRetry(header); total = secs * 1000; target = Date.now() + total;
status.textContent = '429'; status.dataset.status = '429';
q('[data-retry]').textContent = header; q('[data-retry]').dateTime = 'PT' + secs + 'S';
q('[data-reset]').textContent = new Date(target).toLocaleTimeString([], { hour12: false });
state.setAttribute('data-limited', ''); state.textContent = 'Too Many Requests · cooling down';
line('429 too many requests · retry-after: ' + header + ' → cooldown ' + secs + 's', 'text-tcd06-danger');
go.disabled = true; go.style.setProperty('--fill', '100%');
live.textContent = 'Rate limited. Retry available in ' + secs + ' seconds.';
clearTimeout(id); requestAnimationFrame(tick);
});
root.cleanup = () => clearTimeout(id);
})();
```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 Rate Limit Button with Retry-After Countdown
Source: https://codefronts.com/snippets/tailwind-css-countdown-timers/tailwind-rate-limit-button-retry-after-countdown/
What a 429 Too Many Requests should look like in a UI: the button enters a cooldown derived from the Retry-After header and an amber fill drains across its label as the seconds tick away, with the response headers beside it so the number is traceable. New: a five-pip quota meter, a scrolling request log and a status-coloured header table, all from data-* variants.
## HTML
```html
<section class="tcd-06 w-full min-h-svh grid place-items-center bg-tcd06-page bg-[radial-gradient(50%_40%_at_50%_30%,var(--color-tcd06-glow),transparent_70%)] px-3 py-12 font-tcd06-mono text-tcd06-ink" aria-labelledby="tcd-06-title">
<div class="w-full max-w-[640px] overflow-hidden rounded-xl border border-tcd06-rule bg-tcd06-panel shadow-[0_40px_80px_-40px_rgb(0_0_0/0.9)]">
<header class="flex min-w-0 items-center gap-2.5 border-b border-tcd06-rule px-4 py-3">
<span aria-hidden="true" class="flex gap-1.5"><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span><span class="size-2.5 rounded-full bg-tcd06-rule"></span></span>
<h2 id="tcd-06-title" class="ml-1 min-w-0 flex-1 text-[12.5px] font-medium tracking-[-0.01em] [overflow-wrap:anywhere]"><span class="text-tcd06-acc">POST</span> /v2/exports</h2>
<span class="flex-none text-[10.5px] text-tcd06-mut">talon-api · staging</span>
</header>
<div class="flex flex-wrap items-center gap-3 border-b border-tcd06-rule p-4">
<button type="button" data-go class="min-h-11 min-w-[168px] cursor-pointer rounded-md border border-tcd06-rule bg-[linear-gradient(var(--color-tcd06-acc-a),var(--color-tcd06-acc-a))] bg-[length:var(--fill,0%)_100%] bg-no-repeat px-4 text-[12.5px] font-semibold tabular-nums transition-[background-size,border-color,color] duration-1000 ease-linear enabled:hover:border-tcd06-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tcd06-acc disabled:cursor-not-allowed disabled:border-tcd06-acc/45 disabled:text-tcd06-acc motion-reduce:transition-none"><span data-label>Run request</span></button>
<div class="grid min-w-0 flex-[1_1_180px] gap-1.5">
<p role="timer" data-state class="text-[11.5px] text-tcd06-mut data-limited:text-tcd06-acc">Ready · 5 requests per minute</p>
<div aria-hidden="true" data-pips class="flex gap-1">
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
<span class="h-1.5 flex-1 rounded-full bg-tcd06-ok transition-colors data-off:bg-tcd06-rule"></span>
</div>
</div>
</div>
<dl class="grid grid-cols-1 border-b border-tcd06-rule sm:grid-cols-2">
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r"><dt class="text-tcd06-mut">status</dt><dd data-status class="font-semibold data-[status=202]:text-tcd06-ok data-[status=429]:text-tcd06-danger">—</dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">retry-after</dt><dd class="font-semibold"><time datetime="" data-retry>—</time></dd></div>
<div class="flex justify-between gap-3 border-b border-tcd06-rule px-4 py-2.5 text-[11.5px] sm:border-r sm:border-b-0"><dt class="text-tcd06-mut">x-ratelimit-remaining</dt><dd data-left class="font-semibold">5</dd></div>
<div class="flex justify-between gap-3 px-4 py-2.5 text-[11.5px]"><dt class="text-tcd06-mut">x-ratelimit-reset</dt><dd data-reset class="font-semibold">—</dd></div>
</dl>
<ol data-log aria-label="Request log" class="grid max-h-40 gap-1 overflow-auto bg-tcd06-page/60 px-4 py-3 text-[11.5px] leading-relaxed text-tcd06-mut">
<li>$ waiting for first run</li>
</ol>
<output data-live aria-live="polite" class="sr-only"></output>
<p class="border-t border-tcd06-rule px-4 py-3 text-[10.5px] leading-relaxed text-tcd06-mut text-pretty">Retry-After may arrive as delta-seconds or an HTTP-date. Both are parsed; a missing header falls back to a fixed 15s wait.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcd06-page: #000000;
--color-tcd06-page: oklch(0 0 0);
--color-tcd06-glow: #14110a;
--color-tcd06-glow: oklch(0.2 0.03 78);
--color-tcd06-panel: #0c0c0e;
--color-tcd06-panel: oklch(0.16 0.004 280);
--color-tcd06-ink: #f4f4f5;
--color-tcd06-ink: oklch(0.96 0.003 280);
--color-tcd06-mut: #9a9aa3;
--color-tcd06-mut: oklch(0.7 0.01 280);
--color-tcd06-rule: #232328;
--color-tcd06-rule: oklch(0.27 0.006 280);
--color-tcd06-acc: #ffb224;
--color-tcd06-acc: oklch(0.81 0.16 78);
--color-tcd06-acc-a: rgb(255 178 36 / 0.22);
--color-tcd06-danger: #ff5c5c;
--color-tcd06-danger: oklch(0.69 0.19 25);
--color-tcd06-ok: #4ade80;
--color-tcd06-ok: oklch(0.82 0.17 148);
--font-tcd06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tcd-06 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.tcd-06');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const q = (s) => root.querySelector(s);
const go = q('[data-go]'), label = q('[data-label]'), state = q('[data-state]'), log = q('[data-log]');
const live = q('[data-live]'), pips = [...q('[data-pips]').children], status = q('[data-status]');
const LIMIT = 5, FALLBACK = 15;
let left = LIMIT, id = 0, total = 0, target = 0;
const stamp = () => new Date().toLocaleTimeString([], { hour12: false });
const line = (t, cls) => { const li = document.createElement('li'); li.textContent = stamp() + ' ' + t; if (cls) li.className = cls; log.prepend(li); };
const paint = () => { q('[data-left]').textContent = left; pips.forEach((p, i) => p.toggleAttribute('data-off', i >= left)); };
const parseRetry = (v) => Number.isFinite(+v) ? +v : Number.isFinite(Date.parse(v)) ? Math.ceil((Date.parse(v) - Date.now()) / 1000) : FALLBACK;
const tick = () => {
const ms = Math.max(0, target - Date.now()), s = Math.ceil(ms / 1000);
label.textContent = 'Retry in ' + s + 's';
go.style.setProperty('--fill', (ms / total * 100).toFixed(1) + '%');
if (ms > 0) { id = setTimeout(tick, 1000 - (Date.now() % 1000)); return; }
go.disabled = false; label.textContent = 'Run request'; left = LIMIT; paint();
state.removeAttribute('data-limited'); state.textContent = 'Ready · window reset';
line('window reset · ' + LIMIT + ' requests available', 'text-tcd06-ok');
live.textContent = 'Rate limit window reset. You can run the request again.';
};
go.addEventListener('click', () => {
if (go.disabled) return;
if (left > 0) {
left -= 1; paint(); status.textContent = '202'; status.dataset.status = '202';
line('202 accepted · export queued · ' + left + ' left in window');
return;
}
const header = '9';
const secs = parseRetry(header); total = secs * 1000; target = Date.now() + total;
status.textContent = '429'; status.dataset.status = '429';
q('[data-retry]').textContent = header; q('[data-retry]').dateTime = 'PT' + secs + 'S';
q('[data-reset]').textContent = new Date(target).toLocaleTimeString([], { hour12: false });
state.setAttribute('data-limited', ''); state.textContent = 'Too Many Requests · cooling down';
line('429 too many requests · retry-after: ' + header + ' → cooldown ' + secs + 's', 'text-tcd06-danger');
go.disabled = true; go.style.setProperty('--fill', '100%');
live.textContent = 'Rate limited. Retry available in ' + secs + ' seconds.';
clearTimeout(id); requestAnimationFrame(tick);
});
root.cleanup = () => clearTimeout(id);
})();
```How this works
The number comes from the server. When the request is rejected, the script parses Retry-After — seconds or an HTTP-date — and starts a drift-corrected countdown to that moment. The headers table shows the raw values so a developer can trace the label back to the response.
A fill that drains. The button's background is a flat amber gradient sized by a custom property:
<button class="bg-[linear-gradient(var(--color-tcd06-acc-a),var(--color-tcd06-acc-a))]
bg-no-repeat bg-[length:var(--fill,0%)_100%]
transition-[background-size] duration-1000 ease-linear">Each tick sets --fill to the remaining percentage, and the one-second linear transition makes it glide.
Quota you can see. Five pips reflect x-ratelimit-remaining; the script toggles data-off on each spent pip and data-off:bg-tcd06-rule greys it.
One announcement each way. The live region speaks when the limit hits and when the window resets.
Make it yours
- Point
headerat your realresponse.headers.get('Retry-After'). - Change the quota with the
LIMITconstant and add or remove pips. - Use the fallback (
FALLBACK = 15) when your API omitsRetry-After. - Retint the cooldown via
--color-tcd06-acc; the fill uses its-aalpha twin.
Gotchas — read before shipping
Retry-Aftermay be an HTTP-date;Number('Wed, 21 Oct…')isNaN, so parse both forms.- A button that only looks disabled still fires — use the real
disabledattribute. - Do not retry automatically at zero without jitter; many clients waking together recreate the spike.
- Transitioning
background-sizeis not compositor-only; keep it to small controls.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Everything here is baseline on Tailwind v4's floor. color-mix() powers the /45 border modifiers.