Cooldown Button - National Design System

A button behavior that holds a loading state, fires an optional success toast, and runs a live countdown before re-enabling, for rate-limiting resend, retry, and any action you do not want repeated rapidly.

Resend with Success Toast

The full featured pattern for OTP, verification email, and password reset flows where the user needs explicit confirmation the action happened before the cooldown starts counting

Resend Code

Simple Cooldown

For rate-limited retry buttons where you just need to prevent rapid repeats without a confirmation step

Retry

Built-in Features

Auto-initialization

Activates on every .nds-cooldown on the page and on any element added later. No wiring code required.

Optional Loading Phase

Holds the button in the standard data-state="loading" style for the configured number of seconds before the countdown starts.

Live Countdown Label

Swaps the button label to your template every second, with {s} replaced by the seconds remaining, until the cooldown ends.

Built-in Success Toast

Fires a success toast at the bottom of the viewport when the cooldown begins if data-sent-title or data-sent-message is set.

Post-send Label Swap

After the first completed cycle the button can show a different label (for example "Send code" becomes "Resend"). Abort during the loading phase keeps the original label.

Programmatic Control

Trigger the cycle from JS, abort a cooldown in flight, and hook four lifecycle events to wire your own side effects around the built-in behavior.

Usage Guidelines

Best Practices

  • Use for resend flows where the backend imposes a per-user rate limit (OTP, verification email, password reset) and you want the UI to match that limit exactly
  • Use for retry buttons after a failed request, to stop users from hammering an endpoint that is already struggling
  • Use the optional loading phase to model a real network round trip: set data-cooldown-loading to an estimate of the request duration, or trigger NDS.CooldownButton.reset() from your response handler once the real request resolves
  • Do not use this component as a generic submit guard for forms. Use a regular disabled state tied to the form's submission lifecycle instead
  • Do not use it for long cooldowns (over a few minutes). The countdown reads as nagging and ties the user to the page. Show a timestamp and refresh-on-load instead
  • Set data-resend-label when the first action and the repeat action read differently. "Send code" on first use and "Resend" on every cycle after is clearer than leaving "Resend" on a button that has never been clicked
  • Keep countdown templates short. "Resend in 30s" fits; a full sentence does not. The label redraws every second
  • Pair the built-in toast with a concrete confirmation message ("A new code has been sent to your mobile number.") rather than a generic "Success". Users need to know what succeeded
  • For custom toast variants, positions, or multi-step actions, skip data-sent-* and listen for nds:cooldown:triggered. Call NDS.Alert.create yourself with the full option set
  • Duration values under 5 seconds feel abrupt and may not give the toast time to be read. Duration values over 60 seconds should trigger a dedicated "please wait" screen, not a button label

Data Attributes

AttributeDescription
data-cooldownSeconds to hold the cooldown. Required to opt in. Non-positive values skip the cooldown entirely. Read once at wire time; editing after page load has no effect
data-cooldown-loadingSeconds to hold the loading state before the countdown begins. Default 0, which skips the loading phase. Read once at wire time
data-cooldown-labelCountdown text template. {s} is replaced by the seconds remaining. Default {s} (number only)
data-resend-labelLabel to restore after the first completed cycle. Omit to keep the initial label across cycles. A mid-loading reset() always restores the initial label
data-sent-titleTitle of the success toast fired when the cooldown begins. Either this or data-sent-message must be present for a toast to appear
data-sent-messageDescription of the success toast fired when the cooldown begins. Toast uses variant success, position bottom, duration 4000ms

Events

All events bubble and fire on the button element. Listen for them to add custom behavior (analytics, alternate toasts, parallel UI updates) without replacing the built-in flow.

EventFires
nds:cooldown:loadingLoading phase begins. Skipped when data-cooldown-loading is 0 or absent
nds:cooldown:triggeredLoading ends and the cooldown starts. The built-in toast (if configured) fires right after this. Best hook for custom toasts or analytics
nds:cooldown:tickEvery second during the cooldown. event.detail.remaining is the seconds left, including a first tick at the full duration
nds:cooldown:endCooldown completed naturally or reset() was called. Button is re-enabled and the label is restored

JavaScript API

The NDS.CooldownButton API provides programmatic control for dynamically added buttons and for aborting a cooldown in flight. Auto-initialization handles everything for static markup; no JS call is needed for the common case.

// ── Auto-initialization ────────────────────────────── // Every .nds-cooldown on the page is wired on page load. // Elements added to the DOM later are wired automatically. // Call init() manually only if you disabled the loader. NDS.CooldownButton.init(); // ── Trigger the cycle programmatically ─────────────── // Useful when the cooldown should start from a flow other // than the button's own click (e.g. after a form submit). const btn = document.querySelector('#my-resend-btn'); NDS.CooldownButton.start(btn); // ── Abort an in-flight cooldown ────────────────────── // Re-enables the button, clears timers, restores label. // During loading phase: restores the original label. // During cooldown: restores the post-send (data-resend-label) // label if set, otherwise the original. NDS.CooldownButton.reset(btn); // ── Listen for lifecycle events ────────────────────── btn.addEventListener('nds:cooldown:triggered', () => { // Loading is done and the cooldown just started. // Fire a custom toast, log an analytics event, etc. }); btn.addEventListener('nds:cooldown:tick', (e) => { console.log('seconds remaining:', e.detail.remaining); }); btn.addEventListener('nds:cooldown:end', () => { // Button is re-enabled and restored. }); // ── Bind the cooldown to a real request ────────────── // Start the loading phase on click, then reset() early // when the response arrives so the user is not blocked // by a fake delay when the real one was shorter. btn.addEventListener('click', async () => { try { await fetch('/api/resend', { method: 'POST' }); // success: let the cooldown run as configured } catch (err) { // on failure, abort so the user can retry immediately NDS.CooldownButton.reset(btn); } });
Was this page useful?
60% of users said Yes from 2843 Feedbacks