30 CSS Shake Animations19 / 30

CSS + JSMIT licensed

CSS Alert Badge Shake Animation

Badges that refuse to be ignored: a sidebar rail whose alert count rattles on a polite schedule (with a ping halo, while read items sit still), and a CI status pill that keeps twitching until you actually click resolve — state-bound motion that stops the moment the state does.

Published

Live Demo
Try it

The code

<div class="shk-19-group">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-19a" aria-label="Sidebar with rattling alert badge">
  <nav class="shk-19a__rail" aria-label="Demo navigation">
    <a class="shk-19a__item" href="#" onclick="return false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9.5 12 3l9 6.5V21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"></path></svg>Home</a>
    <a class="shk-19a__item is-alert" href="#" onclick="return false" aria-label="Inbox, 4 unread alerts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-6l-2 3h-4l-2-3H2"></path><path d="M5.4 5.1 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.4-6.9A2 2 0 0 0 16.8 4H7.2a2 2 0 0 0-1.8 1.1z"></path></svg>Inbox<span class="shk-19a__badge" aria-hidden="true">4<i class="shk-19a__halo"></i></span></a>
    <a class="shk-19a__item" href="#" onclick="return false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"></rect><path d="M3 10h18M9 4v16"></path></svg>Projects</a>
    <a class="shk-19a__item" href="#" onclick="return false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>Settings</a>
  </nav>
  <p class="shk-19a__hint" aria-hidden="true">the badge rattles ~½s of every 3s — read items hold perfectly still</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
<section class="shk-19b" aria-label="CI status pill that shakes until resolved">
  <div class="shk-19b__bar">
    <span class="shk-19b__repo">meridian/web · <b>main</b></span>
    <span class="shk-19b__pill" id="shk-19b-pill" data-state="failing" aria-live="polite"><i class="shk-19b__dot" aria-hidden="true"></i><span class="shk-19b__count" id="shk-19b-count">3 checks failing</span></span>
    <button class="shk-19b__fix" id="shk-19b-fix" type="button">Re-run checks</button>
  </div>
  <p class="shk-19b__hint" aria-hidden="true">the twitch is bound to data-state="failing" — resolving flips the attribute and ALL motion stops atomically</p>
</section>
</div>
.shk-19-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.shk-19-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.shk-19a,
.shk-19a *,
.shk-19a *::before,
.shk-19a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-19a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-content: center;
  gap: 20px;
  padding: 48px 24px;
  background: linear-gradient(180deg,#13161d,#0d0f14);
  font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}

.shk-19a__rail {
  width: min(280px,86vw);
  padding: 12px;
  border-radius: 18px;
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(165,185,220,.14);
  display: grid;
  gap: 4px;
  box-shadow: 0 40px 80px -45px rgba(0,0,0,.95);
}

.shk-19a__item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
  border-radius: 11px;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  color: rgba(222,230,246,.7);
  transition: background .15s,color .15s;
}

.shk-19a__item svg {
  width: 19px;
  height: 19px;
  flex: none;
}

.shk-19a__item:hover {
  background: rgba(255,255,255,.07);
  color: #eef2fb;
}

.shk-19a__item.is-alert {
  color: #eef2fb;
}

.shk-19a__badge {
  position: relative;
  margin-left: auto;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  display: grid;
  place-items: center;
  font-size: 11.5px;
  font-weight: 800;
  color: #fff;
  background: oklch(0.6 0.21 25);
  border-radius: 999px;
  animation: shk-19a-rattle 3s ease-in-out infinite;
}

.shk-19a__halo {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  border: 2px solid oklch(0.6 0.21 25);
  animation: shk-19a-halo 3s ease-out infinite;
}

.shk-19a__hint {
  text-align: center;
  font-size: 12px;
  color: rgba(222,230,246,.4);
}

@keyframes shk-19a-rattle {
  0%,
    4% {
    translate: 0 0;
    rotate: 0deg;
  }

  6% {
    translate: -1.5px 0;
    rotate: -6deg;
  }

  8.5% {
    translate: 1.5px 0;
    rotate: 5deg;
  }

  11% {
    translate: -1px 0;
    rotate: -3deg;
  }

  13.5% {
    translate: .5px 0;
    rotate: 1.5deg;
  }

  16%,
    100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes shk-19a-halo {
  0%,
    4% {
    opacity: .9;
    scale: 1;
  }

  22% {
    opacity: 0;
    scale: 2;
  }

  100% {
    opacity: 0;
    scale: 2;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-19a__badge,
    .shk-19a__halo {
    animation: none;
  }

  .shk-19a__halo {
    opacity: 0;
  }
}

.shk-19b,
.shk-19b *,
.shk-19b *::before,
.shk-19b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-19b {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-content: center;
  gap: 18px;
  padding: 48px 24px;
  background: #0e1014;
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.shk-19b__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  padding: 15px 20px;
  border-radius: 14px;
  background: #161a21;
  border: 1px solid rgba(170,190,220,.14);
}

.shk-19b__repo {
  font-family: 'JetBrains Mono',ui-monospace,monospace;
  font-size: 13px;
  color: rgba(215,226,245,.6);
}

.shk-19b__repo b {
  color: #e8eefa;
  font-weight: 600;
}

.shk-19b__pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  transition: background .3s,color .3s,border-color .3s;
  border: 1px solid;
}

.shk-19b__pill[data-state="failing"] {
  color: oklch(0.82 0.13 25);
  background: oklch(0.6 0.21 25 / .13);
  border-color: oklch(0.6 0.21 25 / .45);
}

.shk-19b__pill[data-state="running"] {
  color: oklch(0.85 0.12 85);
  background: oklch(0.75 0.14 85 / .12);
  border-color: oklch(0.75 0.14 85 / .4);
}

.shk-19b__pill[data-state="passing"] {
  color: oklch(0.85 0.13 158);
  background: oklch(0.75 0.15 158 / .12);
  border-color: oklch(0.75 0.15 158 / .4);
}

.shk-19b__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
}

.shk-19b__pill[data-state="running"] .shk-19b__dot {
  animation: shk-19b-blink .8s ease-in-out infinite;
}

.shk-19b__pill[data-state="failing"] .shk-19b__count {
  display: inline-block;
  animation: shk-19b-twitch 2.4s ease-in-out infinite;
}

.shk-19b__pill[data-state="passing"] {
  animation: shk-19b-settle .5s cubic-bezier(.34,1.56,.64,1);
}

.shk-19b__fix {
  margin-left: auto;
  padding: 10px 18px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 700;
  color: #0e1014;
  background: #e8eefa;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  transition: filter .2s,scale .12s;
}

.shk-19b__fix:hover {
  filter: brightness(1.08);
}

.shk-19b__fix:active {
  scale: .96;
}

.shk-19b__hint {
  text-align: center;
  font-size: 12px;
  color: rgba(215,226,245,.4);
  max-width: 480px;
  justify-self: center;
}

@keyframes shk-19b-twitch {
  0%,
    6% {
    translate: 0 0;
  }

  7.5% {
    translate: -2px 0;
  }

  9% {
    translate: 2px 0;
  }

  10.5% {
    translate: -1px 0;
  }

  12% {
    translate: 0 0;
  }

  54% {
    translate: 0 0;
  }

  55.5% {
    translate: -2px 0;
  }

  57% {
    translate: 1.5px 0;
  }

  58.5%,
    100% {
    translate: 0 0;
  }
}

@keyframes shk-19b-blink {
  50% {
    opacity: .35;
  }
}

@keyframes shk-19b-settle {
  40% {
    scale: 1.06;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-19b__pill[data-state="failing"] .shk-19b__count,
    .shk-19b__pill[data-state="passing"] {
    animation: none;
  }
}
/* No JavaScript — badge rattle and halo share one 3s loop (burst in the first 16%). Give each badge on a page a different period (2.4s / 3.1s / 3.7s) so alerts never synchronize. */

(function(){
  var pill=document.getElementById('shk-19b-pill'),fix=document.getElementById('shk-19b-fix'),
      count=document.getElementById('shk-19b-count'); if(!pill) return;
  fix.addEventListener('click',function(){
    if(pill.dataset.state!=='failing') return;
    pill.dataset.state='running'; count.textContent='Re-running 3 checks…'; fix.disabled=true; fix.style.opacity='.5';
    setTimeout(function(){
      pill.dataset.state='passing';                 // one attribute write: motion, color, copy all flip
      count.textContent='All checks passing';
      fix.textContent='Resolved'; 
    },1600);
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Shake Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Alert Badge Shake Animation
Source: https://codefronts.com/motion/css-shake-animation/css-alert-badge-shake-animation/

Badges that refuse to be ignored: a sidebar rail whose alert count rattles on a polite schedule (with a ping halo, while read items sit still), and a CI status pill that keeps twitching until you actually click resolve — state-bound motion that stops the moment the state does.
## HTML
```html
<div class="shk-19-group">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-19a" aria-label="Sidebar with rattling alert badge">
  <nav class="shk-19a__rail" aria-label="Demo navigation">
    <a class="shk-19a__item" href="#" onclick="return false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9.5 12 3l9 6.5V21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"></path></svg>Home</a>
    <a class="shk-19a__item is-alert" href="#" onclick="return false" aria-label="Inbox, 4 unread alerts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 12h-6l-2 3h-4l-2-3H2"></path><path d="M5.4 5.1 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.4-6.9A2 2 0 0 0 16.8 4H7.2a2 2 0 0 0-1.8 1.1z"></path></svg>Inbox<span class="shk-19a__badge" aria-hidden="true">4<i class="shk-19a__halo"></i></span></a>
    <a class="shk-19a__item" href="#" onclick="return false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"></rect><path d="M3 10h18M9 4v16"></path></svg>Projects</a>
    <a class="shk-19a__item" href="#" onclick="return false"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>Settings</a>
  </nav>
  <p class="shk-19a__hint" aria-hidden="true">the badge rattles ~½s of every 3s — read items hold perfectly still</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
<section class="shk-19b" aria-label="CI status pill that shakes until resolved">
  <div class="shk-19b__bar">
    <span class="shk-19b__repo">meridian/web · <b>main</b></span>
    <span class="shk-19b__pill" id="shk-19b-pill" data-state="failing" aria-live="polite"><i class="shk-19b__dot" aria-hidden="true"></i><span class="shk-19b__count" id="shk-19b-count">3 checks failing</span></span>
    <button class="shk-19b__fix" id="shk-19b-fix" type="button">Re-run checks</button>
  </div>
  <p class="shk-19b__hint" aria-hidden="true">the twitch is bound to data-state="failing" — resolving flips the attribute and ALL motion stops atomically</p>
</section>
</div>
```
## CSS
```css
.shk-19-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.shk-19-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.shk-19a,
.shk-19a *,
.shk-19a *::before,
.shk-19a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-19a {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-content: center;
  gap: 20px;
  padding: 48px 24px;
  background: linear-gradient(180deg,#13161d,#0d0f14);
  font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}

.shk-19a__rail {
  width: min(280px,86vw);
  padding: 12px;
  border-radius: 18px;
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(165,185,220,.14);
  display: grid;
  gap: 4px;
  box-shadow: 0 40px 80px -45px rgba(0,0,0,.95);
}

.shk-19a__item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
  border-radius: 11px;
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  color: rgba(222,230,246,.7);
  transition: background .15s,color .15s;
}

.shk-19a__item svg {
  width: 19px;
  height: 19px;
  flex: none;
}

.shk-19a__item:hover {
  background: rgba(255,255,255,.07);
  color: #eef2fb;
}

.shk-19a__item.is-alert {
  color: #eef2fb;
}

.shk-19a__badge {
  position: relative;
  margin-left: auto;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  display: grid;
  place-items: center;
  font-size: 11.5px;
  font-weight: 800;
  color: #fff;
  background: oklch(0.6 0.21 25);
  border-radius: 999px;
  animation: shk-19a-rattle 3s ease-in-out infinite;
}

.shk-19a__halo {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  border: 2px solid oklch(0.6 0.21 25);
  animation: shk-19a-halo 3s ease-out infinite;
}

.shk-19a__hint {
  text-align: center;
  font-size: 12px;
  color: rgba(222,230,246,.4);
}

@keyframes shk-19a-rattle {
  0%,
    4% {
    translate: 0 0;
    rotate: 0deg;
  }

  6% {
    translate: -1.5px 0;
    rotate: -6deg;
  }

  8.5% {
    translate: 1.5px 0;
    rotate: 5deg;
  }

  11% {
    translate: -1px 0;
    rotate: -3deg;
  }

  13.5% {
    translate: .5px 0;
    rotate: 1.5deg;
  }

  16%,
    100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

@keyframes shk-19a-halo {
  0%,
    4% {
    opacity: .9;
    scale: 1;
  }

  22% {
    opacity: 0;
    scale: 2;
  }

  100% {
    opacity: 0;
    scale: 2;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-19a__badge,
    .shk-19a__halo {
    animation: none;
  }

  .shk-19a__halo {
    opacity: 0;
  }
}

.shk-19b,
.shk-19b *,
.shk-19b *::before,
.shk-19b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.shk-19b {
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-content: center;
  gap: 18px;
  padding: 48px 24px;
  background: #0e1014;
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.shk-19b__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
  padding: 15px 20px;
  border-radius: 14px;
  background: #161a21;
  border: 1px solid rgba(170,190,220,.14);
}

.shk-19b__repo {
  font-family: 'JetBrains Mono',ui-monospace,monospace;
  font-size: 13px;
  color: rgba(215,226,245,.6);
}

.shk-19b__repo b {
  color: #e8eefa;
  font-weight: 600;
}

.shk-19b__pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  transition: background .3s,color .3s,border-color .3s;
  border: 1px solid;
}

.shk-19b__pill[data-state="failing"] {
  color: oklch(0.82 0.13 25);
  background: oklch(0.6 0.21 25 / .13);
  border-color: oklch(0.6 0.21 25 / .45);
}

.shk-19b__pill[data-state="running"] {
  color: oklch(0.85 0.12 85);
  background: oklch(0.75 0.14 85 / .12);
  border-color: oklch(0.75 0.14 85 / .4);
}

.shk-19b__pill[data-state="passing"] {
  color: oklch(0.85 0.13 158);
  background: oklch(0.75 0.15 158 / .12);
  border-color: oklch(0.75 0.15 158 / .4);
}

.shk-19b__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
}

.shk-19b__pill[data-state="running"] .shk-19b__dot {
  animation: shk-19b-blink .8s ease-in-out infinite;
}

.shk-19b__pill[data-state="failing"] .shk-19b__count {
  display: inline-block;
  animation: shk-19b-twitch 2.4s ease-in-out infinite;
}

.shk-19b__pill[data-state="passing"] {
  animation: shk-19b-settle .5s cubic-bezier(.34,1.56,.64,1);
}

.shk-19b__fix {
  margin-left: auto;
  padding: 10px 18px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 700;
  color: #0e1014;
  background: #e8eefa;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  transition: filter .2s,scale .12s;
}

.shk-19b__fix:hover {
  filter: brightness(1.08);
}

.shk-19b__fix:active {
  scale: .96;
}

.shk-19b__hint {
  text-align: center;
  font-size: 12px;
  color: rgba(215,226,245,.4);
  max-width: 480px;
  justify-self: center;
}

@keyframes shk-19b-twitch {
  0%,
    6% {
    translate: 0 0;
  }

  7.5% {
    translate: -2px 0;
  }

  9% {
    translate: 2px 0;
  }

  10.5% {
    translate: -1px 0;
  }

  12% {
    translate: 0 0;
  }

  54% {
    translate: 0 0;
  }

  55.5% {
    translate: -2px 0;
  }

  57% {
    translate: 1.5px 0;
  }

  58.5%,
    100% {
    translate: 0 0;
  }
}

@keyframes shk-19b-blink {
  50% {
    opacity: .35;
  }
}

@keyframes shk-19b-settle {
  40% {
    scale: 1.06;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-19b__pill[data-state="failing"] .shk-19b__count,
    .shk-19b__pill[data-state="passing"] {
    animation: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — badge rattle and halo share one 3s loop (burst in the first 16%). Give each badge on a page a different period (2.4s / 3.1s / 3.7s) so alerts never synchronize. */

(function(){
  var pill=document.getElementById('shk-19b-pill'),fix=document.getElementById('shk-19b-fix'),
      count=document.getElementById('shk-19b-count'); if(!pill) return;
  fix.addEventListener('click',function(){
    if(pill.dataset.state!=='failing') return;
    pill.dataset.state='running'; count.textContent='Re-running 3 checks…'; fix.disabled=true; fix.style.opacity='.5';
    setTimeout(function(){
      pill.dataset.state='passing';                 // one attribute write: motion, color, copy all flip
      count.textContent='All checks passing';
      fix.textContent='Resolved'; 
    },1600);
  });
})();
```

How this works

Badge shakes are tiny, so they trade amplitude for frequency — 1–2px at high frequency reads as urgency at any size. The rail badge reuses the idle-loop (burst, then silence); the status pill binds an infinite shake to a state class:

.pill[data-state="failing"] .count{
  animation:twitch 2.4s ease-in-out infinite;
}
.pill[data-state="passing"] .count{animation:none}

Binding motion to data-state instead of adding/removing animation classes means the DOM state IS the source of truth — flip the attribute and the motion, color and copy all switch atomically. No cleanup code, no orphaned animations.

Make it yours

  • Badge rattles: ±1.5px translate or ±8° rotate, never both at full strength on a 16px element.
  • Period-stagger multiple badges (2.4s, 3.1s, 3.7s) so a busy sidebar never synchronizes into a seizure.
  • Bind everything to data-state and flip it from your store subscription — one line per state change.
  • A resolving action should kill motion INSTANTLY (state flip), then confirm with a single green pulse.

Gotchas — read before shipping

  • Infinite badge motion is only acceptable while the state is genuinely actionable; 'unread since last week' should decay to static.
  • Shaking a number mid-change is unreadable — pause the twitch, swap the count, resume.
  • aria-live on the pill text, not the count span, so screen readers hear '3 checks failing', not '3'.
  • Don't animate box-shadow for the halo (paint) — scale an ::after ring (compositor).

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome all modern.

Attribute-selector state binding is CSS 2.1 — works everywhere. The ping halo is transform/opacity only, so it stays on the compositor even mid-shake.

Techniques used in this demo

Search CodeFronts

Loading…