Sync State Cycle
A copy-paste indicators component in pure HTML, CSS & vanilla JS. Zero dependencies, framework-agnostic, MIT-licensed.
IndicatorsHTMLJavaScriptCSSany framework
Syncing…
Copy into your project
HTML
<div class="nuda-in2-synccycle" data-state="syncing" role="status" aria-live="polite">
<span class="nuda-in2-synccycle__icon" aria-hidden="true">
<svg class="nuda-in2-synccycle__i-sync" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M21 2v6h-6" />
<path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
<path d="M3 22v-6h6" />
<path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
</svg>
<svg class="nuda-in2-synccycle__i-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
<svg class="nuda-in2-synccycle__i-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M2 2l20 20" />
<path d="M8.5 8.5a7 7 0 0 0 7 7" />
<path d="M5 5a11 11 0 0 1 14 14" />
</svg>
</span>
<span class="nuda-in2-synccycle__text">Syncing…</span>
</div>JavaScript
/* Sync State Cycle — vanilla JS
Rotates through syncing -> synced -> offline. Updates the
accessible text so screen readers announce the real status. */
(function () {
var STATES = [
{ key: "syncing", text: "Syncing…", hold: 2200 },
{ key: "synced", text: "Synced", hold: 3000 },
{ key: "offline", text: "Offline", hold: 2600 },
];
document.querySelectorAll(".nuda-in2-synccycle").forEach(function (el) {
var i = 0;
var label = el.querySelector(".nuda-in2-synccycle__text");
function step() {
var s = STATES[i % STATES.length];
el.setAttribute("data-state", s.key);
label.textContent = s.text;
i++;
setTimeout(step, s.hold);
}
setTimeout(step, STATES[0].hold);
});
})();CSS
.nuda-in2-synccycle {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 12px;
background: #161616;
border: 1px solid rgba(255,255,255,.1);
border-radius: 99px;
font-size: 11px;
font-weight: 600;
color: #cfcfcf;
}
.nuda-in2-synccycle__icon {
position: relative;
width: 14px;
height: 14px;
flex: 0 0 auto;
}
.nuda-in2-synccycle__icon svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: none;
}
.nuda-in2-synccycle[data-state="syncing"] .nuda-in2-synccycle__i-sync {
display: block;
color: #e4ff54;
animation: _nuda-in2synccycle-spin 1s linear infinite;
}
.nuda-in2-synccycle[data-state="synced"] .nuda-in2-synccycle__i-ok {
display: block;
color: #6ee7b7;
}
.nuda-in2-synccycle[data-state="offline"] .nuda-in2-synccycle__i-off {
display: block;
color: #f87171;
}
.nuda-in2-synccycle[data-state="syncing"] .nuda-in2-synccycle__text {
color: #e4ff54;
}
.nuda-in2-synccycle[data-state="synced"] .nuda-in2-synccycle__text {
color: #6ee7b7;
}
.nuda-in2-synccycle[data-state="offline"] .nuda-in2-synccycle__text {
color: #f87171;
}
@keyframes _nuda-in2synccycle-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion:reduce) {
.nuda-in2-synccycle__i-sync {
animation: none !important;
}
}
How to use Sync State Cycle
Paste the HTML where you need it and the CSS into a global stylesheet (or a <style> tag). Every class is prefixed nuda- so it never collides with Tailwind or your own styles. Tweak the CSS custom properties to match your design system.
Works in React, Vue, Svelte, Astro, Next.js, Nuxt, Laravel Blade, Django, Rails — or a single .html file. No npm install, no build step.