Skip to content

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.

More indicators components

← Browse all NudaUI components