Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bsrate-card">
<div class="card-body p-4">
<label class="form-label small fw-semibold">Invite a teammate</label>
<div class="input-group mb-2">
<input type="email" class="form-control" id="bsrateEmail" placeholder="teammate@company.com">
<button type="button" class="btn btn-dark fw-bold" id="bsrateBtn">Send invite</button>
</div>
<p class="small text-muted mb-0" id="bsrateStatus">You can send up to 3 invites per minute.</p>
</div>
</div>
</div>.bsrate-card { width: 380px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
#bsrateStatus.text-danger { color: #dc3545 !important; }
#bsrateStatus.text-success { color: #198754 !important; }const LIMIT = 3;
const WINDOW_MS = 60 * 1000;
const DEMO_WINDOW_MS = 8 * 1000; // sped up so the cooldown is actually observable here
const btn = document.getElementById('bsrateBtn');
const email = document.getElementById('bsrateEmail');
const status = document.getElementById('bsrateStatus');
// Timestamps of recent sends. Rate limiting by "count within a sliding
// window" rather than a flat cooldown after every click — the first LIMIT
// clicks are instant, and only clicking faster than the limit allows starts
// blocking, which matches how real API rate limits typically behave.
let sentAt = [];
let cooldownTimer = null;
function prune() {
const cutoff = Date.now() - DEMO_WINDOW_MS;
sentAt = sentAt.filter(t => t > cutoff);
}
function msUntilNextSlot() {
prune();
if (sentAt.length < LIMIT) return 0;
return sentAt[0] + DEMO_WINDOW_MS - Date.now();
}
function updateButton() {
const wait = msUntilNextSlot();
clearInterval(cooldownTimer);
if (wait <= 0) {
btn.disabled = false;
btn.textContent = 'Send invite';
return;
}
btn.disabled = true;
const tick = () => {
const remaining = Math.ceil(msUntilNextSlot() / 1000);
if (remaining <= 0) {
clearInterval(cooldownTimer);
updateButton();
return;
}
btn.textContent = 'Wait ' + remaining + 's';
};
tick();
cooldownTimer = setInterval(tick, 250);
}
btn.addEventListener('click', () => {
if (!email.value.trim()) {
status.textContent = 'Enter an email address first.';
status.className = 'small text-danger mb-0';
return;
}
if (msUntilNextSlot() > 0) return;
sentAt.push(Date.now());
status.textContent = 'Invite sent to ' + email.value.trim() + '.';
status.className = 'small text-success mb-0';
email.value = '';
updateButton();
});
updateButton();Bootstrap Rate-Limited Action Button — Free HTML CSS JS Snippet
Bootstrap Rate-Limited Action Button · Buttons · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Bootstrap Rate-Limited Action Button — HTML, CSS & JavaScript

This models a genuinely different, more realistic pattern than a flat "disable for N seconds after every click" cooldown — sentAt records the timestamp of every recent click, and prune() discards any older than the sliding window before every check, so the button only ever blocks once LIMIT clicks have genuinely happened within that window. The first few clicks in a row fire instantly, one after another, exactly like a real API token-bucket or sliding-window rate limiter behaves — not like a resend-code button that always makes you wait the same fixed delay even for your very first click.
msUntilNextSlot() is the one function both the click handler and the countdown display read from — it returns 0 when there's room under the limit, or the exact number of milliseconds until the oldest recorded click ages out of the window and frees up a slot. Because both places call the same function, the button's disabled state and the countdown number it displays can never disagree about how long is actually left.
The countdown ticks every 250ms rather than a full second, re-deriving the remaining time from msUntilNextSlot() on every tick instead of counting down a separately stored number — which is what keeps the displayed countdown accurate even if the tab was backgrounded and timers were throttled, since it's always computed fresh from real timestamps rather than decremented blindly.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Hand this snippet to an AI coding assistant like Claude and ask it to persist the sentAt timestamps in localStorage so the rate limit survives a page refresh, or to sync the client-side limit with a real API's rate-limit response headers (like X-RateLimit-Remaining and X-RateLimit-Reset) instead of tracking clicks purely client-side.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a Bootstrap 5.3 button with client-side sliding-window rate limiting, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.
Requirements:
- An input field and an action button (e.g. "Send invite") that allows up to a fixed number of clicks (e.g. 3) within a rolling time window, tracked as an array of click timestamps rather than a single flat cooldown.
- Once the limit is reached within the current window, disable the button and show a live countdown (e.g. "Wait 7s") counting down to the moment the oldest click ages out of the window and frees up a slot — recompute the remaining time from real timestamps on every tick, not by decrementing a stored number.
- The button must re-enable itself automatically and immediately once a slot becomes available, with no manual reset action required.
- An empty or invalid input submission must be rejected with a validation message and must not count against the rate limit at all.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Type an email and click "Send invite" three times in a row quicklyAll three sends go through instantly, one after another, with no waiting.
- 2Try clicking a fourth time immediatelyThe button disables and shows a live "Wait Ns" countdown instead of sending.
- 3Watch the countdownIt ticks down in real time until the oldest of your three sends ages out of the window.
- 4Wait for the countdown to reach zeroThe button re-enables itself automatically, ready for another send.
- 5Click "Send invite" with the email field emptyIt shows a validation message instead of counting toward the rate limit at all.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A flat cooldown blocks every single click equally, even the very first one in a session. This sliding-window approach allows a genuine burst of clicks up to the limit before blocking anything, only kicking in once that limit is actually exceeded — much closer to how real API rate limits (and their 429 responses) typically behave.
A plain decrementing counter can drift if the browser throttles background tab timers; recomputing msUntilNextSlot() from real Date.now() timestamps on every tick keeps the displayed countdown accurate regardless of any timer throttling that occurred in between.
No — the empty-email check happens before msUntilNextSlot() is even consulted for blocking, and a rejected empty submission never gets pushed into sentAt, so it doesn't consume one of the limited slots.
Yes. Keep sentAt in a ref (not state, since it updates on a timer without needing a full re-render for every millisecond) and mirror the computed disabled/label values into component state on each tick, the same way this snippet updates the DOM directly.