DNS Propagation Checker Widget — Free HTML CSS JS Snippet
DNS Propagation Checker Widget · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="demo">
<div class="tile">
<div class="tile-head">
<div>
<h3>DNS propagation</h3>
<p class="record-line"><span class="record-type">A</span> app.example.com <span class="arrow">→</span> 203.0.113.42</p>
</div>
<button class="recheck-btn" id="recheckBtn">Recheck</button>
</div>
<div class="progress-row">
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<span class="progress-text" id="progressText">0 / 8 resolvers</span>
</div>
<ul class="resolver-list" id="resolverList"></ul>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
.tile { width: 400px; max-width: 100%; background: #fff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 20px 22px; display: flex; flex-direction: column; gap: 14px; }
.tile-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.tile-head h3 { font-size: 15px; font-weight: 800; color: #0f172a; margin-bottom: 4px; }
.record-line { font-size: 11.5px; color: #64748b; font-family: ui-monospace, 'SF Mono', monospace; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.record-type { background: #eef2ff; color: #4f46e5; font-weight: 800; padding: 1px 6px; border-radius: 5px; font-size: 10.5px; }
.arrow { color: #cbd5e1; }
.recheck-btn { border: 1px solid #e2e8f0; background: #fff; color: #4f46e5; font-size: 11.5px; font-weight: 700; padding: 7px 13px; border-radius: 8px; cursor: pointer; flex-shrink: 0; transition: background 0.12s, opacity 0.15s; }
.recheck-btn:hover:not(:disabled) { background: #f8fafc; }
.recheck-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.progress-row { display: flex; align-items: center; gap: 10px; }
.progress-bar { flex: 1; height: 7px; border-radius: 999px; background: #f1f5f9; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #6366f1, #8b5cf6); transition: width 0.4s ease; }
.progress-fill.complete { background: linear-gradient(90deg, #16a34a, #22c55e); }
.progress-text { font-size: 11px; font-weight: 700; color: #94a3b8; white-space: nowrap; font-variant-numeric: tabular-nums; }
.resolver-list { list-style: none; display: flex; flex-direction: column; gap: 6px; }
.resolver-row { display: flex; align-items: center; gap: 10px; padding: 9px 11px; border-radius: 10px; background: #f8fafc; opacity: 0; transform: translateY(4px); transition: opacity 0.3s ease, transform 0.3s ease, background 0.2s; }
.resolver-row.shown { opacity: 1; transform: translateY(0); }
.resolver-row.propagated { background: #f0fdf4; }
.resolver-dot { width: 8px; height: 8px; border-radius: 50%; background: #cbd5e1; flex-shrink: 0; }
.resolver-row.pending .resolver-dot { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
.resolver-row.propagated .resolver-dot { background: #16a34a; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.resolver-name { flex: 1; font-size: 12.5px; font-weight: 700; color: #0f172a; }
.resolver-loc { font-size: 11px; color: #94a3b8; }
.resolver-status { font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.03em; padding: 3px 8px; border-radius: 6px; flex-shrink: 0; }
.resolver-row.pending .resolver-status { background: #fffbeb; color: #b45309; }
.resolver-row.propagated .resolver-status { background: #dcfce7; color: #15803d; }var RESOLVERS = [
{ name: 'Google', loc: 'Iowa, US', ip: '8.8.8.8', delay: 600 },
{ name: 'Cloudflare', loc: 'San Jose, US', ip: '1.1.1.1', delay: 850 },
{ name: 'Quad9', loc: 'Zurich, CH', ip: '9.9.9.9', delay: 1400 },
{ name: 'OpenDNS', loc: 'London, UK', ip: '208.67.222.222', delay: 1750 },
{ name: 'NTT', loc: 'Tokyo, JP', ip: '129.250.35.250', delay: 2300 },
{ name: 'Comodo', loc: 'Sydney, AU', ip: '8.26.56.26', delay: 2900 },
{ name: 'DNS.WATCH', loc: 'Frankfurt, DE', ip: '84.200.69.80', delay: 3500 },
{ name: 'Level3', loc: 'S\u00e3o Paulo, BR', ip: '209.244.0.3', delay: 4100 },
];
var listEl = document.getElementById('resolverList');
var fillEl = document.getElementById('progressFill');
var textEl = document.getElementById('progressText');
var recheckBtn = document.getElementById('recheckBtn');
var timers = [];
function buildRows() {
listEl.innerHTML = RESOLVERS.map(function (r, i) {
return '<li class="resolver-row pending" data-i="' + i + '">' +
'<span class="resolver-dot"></span>' +
'<span class="resolver-name">' + r.name + '</span>' +
'<span class="resolver-loc">' + r.loc + '</span>' +
'<span class="resolver-status">Checking</span>' +
'</li>';
}).join('');
requestAnimationFrame(function () {
listEl.querySelectorAll('.resolver-row').forEach(function (row) { row.classList.add('shown'); });
});
}
function updateProgress(done, total) {
var pct = (done / total) * 100;
fillEl.style.width = pct + '%';
fillEl.classList.toggle('complete', done === total);
textEl.textContent = done + ' / ' + total + ' resolvers' + (done === total ? ' \u2014 fully propagated' : '');
}
function runCheck() {
timers.forEach(clearTimeout);
timers = [];
recheckBtn.disabled = true;
recheckBtn.textContent = 'Checking\u2026';
buildRows();
updateProgress(0, RESOLVERS.length);
var doneCount = 0;
RESOLVERS.forEach(function (r, i) {
var t = setTimeout(function () {
var row = listEl.querySelector('.resolver-row[data-i="' + i + '"]');
if (!row) return;
row.classList.remove('pending');
row.classList.add('propagated');
row.querySelector('.resolver-status').textContent = 'Resolved';
doneCount++;
updateProgress(doneCount, RESOLVERS.length);
if (doneCount === RESOLVERS.length) {
recheckBtn.disabled = false;
recheckBtn.textContent = 'Recheck';
}
}, r.delay);
timers.push(t);
});
}
recheckBtn.addEventListener('click', runCheck);
runCheck();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
DNS Propagation Checker Widget — Staggered Resolver Checks & Live Progress Bar

After changing a DNS record, the honest answer to "is it live yet" is "it depends which resolver you ask" — propagation isn't instantaneous or uniform, it trickles out across the world's DNS resolvers over minutes to hours depending on TTLs and caching. This widget models that reality directly: instead of a single pass/fail check, it queries a list of named global resolvers one at a time with staggered, realistic delays, letting each one settle into a "resolved" state independently while an overall progress bar tracks how much of the world has caught up.
Staggered delays that mimic real geographic variance
Each resolver object carries its own delay value in RESOLVERS, deliberately spread from 600ms (Google's well-connected Iowa infrastructure) to 4100ms (a resolver on the other side of the world) rather than resolving all eight simultaneously. runCheck() schedules one setTimeout per resolver at its own delay, so the list visibly fills in from nearest/fastest to farthest/slowest — the same pattern a real propagation check exhibits, where nearby resolvers with recently-refreshed caches often update well before ones on a different continent.
A pulsing dot signals "still checking," not silence
Rows in the pending state get a resolver-dot with a pulse keyframe animation (opacity oscillating via animation: pulse 1s ease-in-out infinite), giving each unresolved row a subtle heartbeat that communicates "this is actively being checked" rather than looking stalled or broken while its timer counts down. Once a resolver's timer fires, the dot switches to solid green and the pulse animation implicitly stops (since .propagated no longer carries the pending class that triggers it).
Progress text and bar color both shift at full completion
updateProgress() writes a "X / Y resolvers" count that, only once done === total, appends " — fully propagated" and swaps the bar's gradient from indigo-purple to green via a .complete class — a single extra piece of state (full completion) gets its own distinct visual treatment rather than the bar simply reaching 100% width in the same color it always was, making the "done" moment more noticeable at a glance.
Recheck cancels in-flight timers before restarting
Clicking #recheckBtn calls timers.forEach(clearTimeout) before scheduling a fresh batch — without this, clicking recheck mid-check would leave the previous run's timers still pending, potentially firing state updates against rows that buildRows() had already replaced, or double-counting doneCount from two overlapping runs. The button also disables itself for the duration of a check, since a real DNS lookup service would reasonably rate-limit or simply be unable to serve overlapping check requests from the same client.
Row entrance uses a fade-and-rise reveal, not an instant list swap
buildRows() inserts all rows with opacity: 0 and adds the .shown class inside a requestAnimationFrame callback, the same two-step technique used elsewhere in this library to guarantee the initial hidden state actually paints before the transition to visible begins — otherwise the browser could coalesce both style writes and skip the fade-in entirely.
Step by step
How to Use
- 1Watch resolvers resolve one at a timeEach resolver row switches from a pulsing "Checking" state to a solid green "Resolved" state at its own staggered delay, simulating real-world propagation timing.
- 2Watch the progress bar and textThe bar fills and the count updates as each resolver completes, turning green with a "fully propagated" label once all resolvers are done.
- 3Click "Recheck"Cancels any in-flight timers, rebuilds the resolver list, and reruns the staggered check from the start — the button disables itself for the duration.
- 4Replace the record being checkedUpdate the record type, hostname, and target value shown in the header .record-line.
- 5Replace RESOLVERS with real lookupsSwap the setTimeout-based simulation for real DNS-over-HTTPS queries (for example via Cloudflare's or Google's DoH endpoints) against each named resolver, updating each row when its real response returns.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a Tailwind CSS version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
No — RESOLVERS is a static demo array and each resolver "resolves" after a fixed setTimeout delay rather than performing a real network lookup. For production use, replace the timers with real DNS-over-HTTPS queries (Cloudflare's or Google's DoH endpoints, for example) issued to each named resolver and update each row when its actual response arrives.
Each resolver object in RESOLVERS carries its own delay value, deliberately varied to mimic how real DNS propagation is uneven across the world — a nearby, well-connected resolver typically shows an updated record before a resolver on another continent with a different cache refresh schedule.
The button is disabled for the duration of a check, so this cannot happen through the UI. Internally, runCheck() also clears every previously scheduled timer via timers.forEach(clearTimeout) before starting a new batch, which prevents a stale in-flight run from firing state updates after a fresh check has already rebuilt the resolver list.
Rows with the pending class include a resolver-dot element with a CSS animation: pulse 1s ease-in-out infinite rule that oscillates its opacity. Once a resolver's timer fires, the row loses the pending class and gains propagated, which switches the dot to a solid green fill with no animation, visually distinguishing "still checking" from "confirmed resolved."
updateProgress() only adds the .complete class — which swaps the bar's gradient from indigo-purple to green — once the done count equals the total resolver count. Reserving the fully-complete color exclusively for the fully-complete state makes that specific moment stand out more clearly than if the bar simply approached 100% in its normal color.
Add or remove objects in the RESOLVERS array, each with a name, loc, ip, and delay. The row-building, progress-counting, and staggered-timer logic all iterate over this array directly, so no other code changes are needed to change the resolver count.