30 CSS Shake Animations03 / 30

CSS + JSMIT licensed

CSS Password Error Shake Effect

The two password shakes every product ends up needing: a desktop unlock screen whose field recoils on a wrong password (with a caps-lock warning, because that's why half of all passwords fail), and a mobile-style 4-digit PIN pad whose dots rattle and flush red before clearing for another try.

Published

Live Demo
Try it

The code

<div class="shk-03-group">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400..800&display=swap" rel="stylesheet">
<section class="shk-03a" aria-label="Unlock screen with wrong-password shake">
  <div class="shk-03a__panel">
    <div class="shk-03a__avatar" id="shk-03a-avatar" aria-hidden="true"><span>KM</span></div>
    <p class="shk-03a__name">Kai Moreau</p>
    <form class="shk-03a__row" id="shk-03a-form">
      <input class="shk-03a__input" id="shk-03a-pass" type="password" placeholder="Enter password" aria-label="Password" autocomplete="off">
      <button class="shk-03a__go" type="submit" aria-label="Unlock">→</button>
    </form>
    <p class="shk-03a__caps" id="shk-03a-caps" hidden>⇪ Caps Lock is on</p>
    <p class="shk-03a__status" id="shk-03a-status" aria-live="polite">Hint: the password is <b>demo123</b></p>
  </div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-03b" aria-label="PIN pad with wrong-code shake">
  <div class="shk-03b__phone">
    <p class="shk-03b__label" id="shk-03b-label" aria-live="polite">Enter passcode · try 2468</p>
    <div class="shk-03b__dots" id="shk-03b-dots" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
    <div class="shk-03b__pad" id="shk-03b-pad">
      <button type="button">1</button><button type="button">2</button><button type="button">3</button>
      <button type="button">4</button><button type="button">5</button><button type="button">6</button>
      <button type="button">7</button><button type="button">8</button><button type="button">9</button>
      <span></span><button type="button">0</button><button type="button" data-del aria-label="Delete">⌫</button>
    </div>
  </div>
</section>
</div>
.shk-03-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

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

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

.shk-03a {
  --err: oklch(0.68 0.2 25);
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(90% 120% at 50% 110%,#2b3040,#101320 55%);
  font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}

.shk-03a__panel {
  display: grid;
  justify-items: center;
  gap: 12px;
  text-align: center;
}

.shk-03a__avatar {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(150deg,oklch(0.7 0.13 260),oklch(0.55 0.16 300));
  border: 2px solid rgba(255,255,255,.25);
  box-shadow: 0 20px 50px -20px rgba(0,0,0,.8);
}

.shk-03a__avatar span {
  font-size: 26px;
  font-weight: 800;
  color: #fff;
  letter-spacing: .02em;
}

.shk-03a__avatar.is-wrong {
  animation: shk-03a-head .5s cubic-bezier(.36,.07,.19,.97);
}

.shk-03a__name {
  font-size: 16px;
  font-weight: 700;
  color: #eef1fa;
}

.shk-03a__row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: min(300px,84vw);
  padding: 5px 5px 5px 16px;
  background: rgba(255,255,255,.09);
  border: 1.5px solid rgba(255,255,255,.14);
  border-radius: 999px;
  backdrop-filter: blur(10px);
  transition: border-color .2s;
}

.shk-03a__row:focus-within {
  border-color: rgba(255,255,255,.4);
}

.shk-03a__row.is-wrong {
  border-color: var(--err);
  animation: shk-03a-shake .45s cubic-bezier(.36,.07,.19,.97);
}

.shk-03a__input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: none;
  font: inherit;
  font-size: 14.5px;
  color: #eef1fa;
  letter-spacing: .08em;
}

.shk-03a__input::placeholder {
  color: rgba(238,241,250,.4);
  letter-spacing: 0;
}

.shk-03a__go {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 0;
  cursor: pointer;
  font-size: 15px;
  color: #101320;
  background: #eef1fa;
  transition: transform .15s;
}

.shk-03a__go:active {
  transform: scale(.92);
}

.shk-03a__caps {
  font-size: 12px;
  font-weight: 700;
  color: oklch(0.83 0.14 85);
  background: oklch(0.83 0.14 85 / .12);
  border: 1px solid oklch(0.83 0.14 85 / .3);
  padding: 4px 12px;
  border-radius: 999px;
}

.shk-03a__status {
  font-size: 12.5px;
  color: rgba(238,241,250,.5);
  min-height: 18px;
}

.shk-03a__status b {
  color: rgba(238,241,250,.8);
}

.shk-03a__status.err {
  color: oklch(0.8 0.12 25);
}

.shk-03a__status.ok {
  color: oklch(0.85 0.12 158);
}

@keyframes shk-03a-shake {
  20% {
    translate: -10px 0;
  }

  40% {
    translate: 8px 0;
  }

  60% {
    translate: -6px 0;
  }

  80% {
    translate: 3px 0;
  }
}

@keyframes shk-03a-head {
  20% {
    rotate: -6deg;
  }

  40% {
    rotate: 5deg;
  }

  60% {
    rotate: -3deg;
  }

  80% {
    rotate: 2deg;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-03a__row.is-wrong,
    .shk-03a__avatar.is-wrong {
    animation: none;
  }
}

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

.shk-03b {
  --err: oklch(0.68 0.21 25);
  --ok: oklch(0.78 0.16 160);
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #07080c;
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.shk-03b__phone {
  display: grid;
  justify-items: center;
  gap: 26px;
  padding: 44px 38px;
  border-radius: 34px;
  background: linear-gradient(180deg,#0e1018,#0a0c12);
  border: 1px solid rgba(160,180,220,.14);
  box-shadow: 0 60px 120px -60px rgba(0,0,0,1);
}

.shk-03b__label {
  font-size: 13.5px;
  color: rgba(225,232,248,.65);
  letter-spacing: .02em;
  min-height: 20px;
}

.shk-03b__label.err {
  color: oklch(0.8 0.13 25);
}

.shk-03b__label.ok {
  color: var(--ok);
}

.shk-03b__dots {
  display: flex;
  gap: 18px;
}

.shk-03b__dots i {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid rgba(225,232,248,.45);
  transition: background .15s,border-color .15s,scale .15s;
}

.shk-03b__dots i.fill {
  background: #e1e8f8;
  border-color: #e1e8f8;
  scale: 1.1;
}

.shk-03b__dots.wrong {
  animation: shk-03b-rattle .5s cubic-bezier(.36,.07,.19,.97);
}

.shk-03b__dots.wrong i {
  background: var(--err);
  border-color: var(--err);
}

.shk-03b__dots.right i {
  background: var(--ok);
  border-color: var(--ok);
}

.shk-03b__pad {
  display: grid;
  grid-template-columns: repeat(3,72px);
  gap: 14px;
}

.shk-03b__pad button {
  height: 72px;
  border-radius: 50%;
  border: 1px solid rgba(160,180,220,.16);
  background: rgba(255,255,255,.05);
  color: #e1e8f8;
  font: inherit;
  font-size: 24px;
  font-weight: 500;
  cursor: pointer;
  transition: background .12s,scale .12s;
}

.shk-03b__pad button:hover {
  background: rgba(255,255,255,.1);
}

.shk-03b__pad button:active {
  background: rgba(255,255,255,.22);
  scale: .94;
}

.shk-03b__pad [data-del] {
  font-size: 19px;
}

@keyframes shk-03b-rattle {
  15% {
    translate: -12px 0;
  }

  30% {
    translate: 10px 0;
  }

  45% {
    translate: -8px 0;
  }

  60% {
    translate: 6px 0;
  }

  75% {
    translate: -3px 0;
  }

  90% {
    translate: 1px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-03b__dots.wrong {
    animation: none;
  }
}
(function(){
  var form=document.getElementById('shk-03a-form'); if(!form) return;
  var row=form,input=document.getElementById('shk-03a-pass'),caps=document.getElementById('shk-03a-caps'),
      status=document.getElementById('shk-03a-status'),avatar=document.getElementById('shk-03a-avatar'),tries=0;
  function retrigger(el,cls){ el.classList.remove(cls); requestAnimationFrame(function(){ requestAnimationFrame(function(){ el.classList.add(cls); }); }); }
  row.addEventListener('animationend',function(){ row.classList.remove('is-wrong'); avatar.classList.remove('is-wrong'); });
  input.addEventListener('keyup',function(e){ if(e.getModifierState) caps.hidden=!e.getModifierState('CapsLock'); });
  form.addEventListener('submit',function(e){
    e.preventDefault();
    if(input.value==='demo123'){ status.className='shk-03a__status ok'; status.textContent='Welcome back, Kai.'; return; }
    tries++;
    retrigger(row,'is-wrong'); retrigger(avatar,'is-wrong');
    status.className='shk-03a__status err';
    status.textContent='Wrong password ('+tries+(tries===1?' try':' tries')+'). Hint: demo123';
    input.select();
  });
})();

(function(){
  var pad=document.getElementById('shk-03b-pad'); if(!pad) return;
  var dots=document.getElementById('shk-03b-dots'),label=document.getElementById('shk-03b-label'),
      pins=dots.querySelectorAll('i'),buf='',locked=false;
  function paint(){ pins.forEach(function(d,i){ d.classList.toggle('fill',i<buf.length); }); }
  dots.addEventListener('animationend',function(){
    dots.classList.remove('wrong'); buf=''; paint(); locked=false;   // clear AFTER the rattle
  });
  pad.addEventListener('click',function(e){
    var b=e.target.closest('button'); if(!b||locked) return;
    if(b.hasAttribute('data-del')){ buf=buf.slice(0,-1); paint(); return; }
    if(buf.length>=4) return;
    buf+=b.textContent.trim(); paint();
    if(buf.length<4) return;
    if(buf==='2468'){ dots.classList.add('right'); label.className='shk-03b__label ok'; label.textContent='Unlocked. Welcome back.'; locked=true; return; }
    locked=true; dots.classList.add('wrong');
    label.className='shk-03b__label err'; label.textContent='Wrong passcode — try 2468';
  });
})();
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 Password Error Shake Effect
Source: https://codefronts.com/motion/css-shake-animation/css-password-error-shake-effect/

The two password shakes every product ends up needing: a desktop unlock screen whose field recoils on a wrong password (with a caps-lock warning, because that's why half of all passwords fail), and a mobile-style 4-digit PIN pad whose dots rattle and flush red before clearing for another try.
## HTML
```html
<div class="shk-03-group">
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400..800&display=swap" rel="stylesheet">
<section class="shk-03a" aria-label="Unlock screen with wrong-password shake">
  <div class="shk-03a__panel">
    <div class="shk-03a__avatar" id="shk-03a-avatar" aria-hidden="true"><span>KM</span></div>
    <p class="shk-03a__name">Kai Moreau</p>
    <form class="shk-03a__row" id="shk-03a-form">
      <input class="shk-03a__input" id="shk-03a-pass" type="password" placeholder="Enter password" aria-label="Password" autocomplete="off">
      <button class="shk-03a__go" type="submit" aria-label="Unlock">→</button>
    </form>
    <p class="shk-03a__caps" id="shk-03a-caps" hidden>⇪ Caps Lock is on</p>
    <p class="shk-03a__status" id="shk-03a-status" aria-live="polite">Hint: the password is <b>demo123</b></p>
  </div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-03b" aria-label="PIN pad with wrong-code shake">
  <div class="shk-03b__phone">
    <p class="shk-03b__label" id="shk-03b-label" aria-live="polite">Enter passcode · try 2468</p>
    <div class="shk-03b__dots" id="shk-03b-dots" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
    <div class="shk-03b__pad" id="shk-03b-pad">
      <button type="button">1</button><button type="button">2</button><button type="button">3</button>
      <button type="button">4</button><button type="button">5</button><button type="button">6</button>
      <button type="button">7</button><button type="button">8</button><button type="button">9</button>
      <span></span><button type="button">0</button><button type="button" data-del aria-label="Delete">⌫</button>
    </div>
  </div>
</section>
</div>
```
## CSS
```css
.shk-03-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

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

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

.shk-03a {
  --err: oklch(0.68 0.2 25);
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(90% 120% at 50% 110%,#2b3040,#101320 55%);
  font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}

.shk-03a__panel {
  display: grid;
  justify-items: center;
  gap: 12px;
  text-align: center;
}

.shk-03a__avatar {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(150deg,oklch(0.7 0.13 260),oklch(0.55 0.16 300));
  border: 2px solid rgba(255,255,255,.25);
  box-shadow: 0 20px 50px -20px rgba(0,0,0,.8);
}

.shk-03a__avatar span {
  font-size: 26px;
  font-weight: 800;
  color: #fff;
  letter-spacing: .02em;
}

.shk-03a__avatar.is-wrong {
  animation: shk-03a-head .5s cubic-bezier(.36,.07,.19,.97);
}

.shk-03a__name {
  font-size: 16px;
  font-weight: 700;
  color: #eef1fa;
}

.shk-03a__row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: min(300px,84vw);
  padding: 5px 5px 5px 16px;
  background: rgba(255,255,255,.09);
  border: 1.5px solid rgba(255,255,255,.14);
  border-radius: 999px;
  backdrop-filter: blur(10px);
  transition: border-color .2s;
}

.shk-03a__row:focus-within {
  border-color: rgba(255,255,255,.4);
}

.shk-03a__row.is-wrong {
  border-color: var(--err);
  animation: shk-03a-shake .45s cubic-bezier(.36,.07,.19,.97);
}

.shk-03a__input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: none;
  font: inherit;
  font-size: 14.5px;
  color: #eef1fa;
  letter-spacing: .08em;
}

.shk-03a__input::placeholder {
  color: rgba(238,241,250,.4);
  letter-spacing: 0;
}

.shk-03a__go {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 0;
  cursor: pointer;
  font-size: 15px;
  color: #101320;
  background: #eef1fa;
  transition: transform .15s;
}

.shk-03a__go:active {
  transform: scale(.92);
}

.shk-03a__caps {
  font-size: 12px;
  font-weight: 700;
  color: oklch(0.83 0.14 85);
  background: oklch(0.83 0.14 85 / .12);
  border: 1px solid oklch(0.83 0.14 85 / .3);
  padding: 4px 12px;
  border-radius: 999px;
}

.shk-03a__status {
  font-size: 12.5px;
  color: rgba(238,241,250,.5);
  min-height: 18px;
}

.shk-03a__status b {
  color: rgba(238,241,250,.8);
}

.shk-03a__status.err {
  color: oklch(0.8 0.12 25);
}

.shk-03a__status.ok {
  color: oklch(0.85 0.12 158);
}

@keyframes shk-03a-shake {
  20% {
    translate: -10px 0;
  }

  40% {
    translate: 8px 0;
  }

  60% {
    translate: -6px 0;
  }

  80% {
    translate: 3px 0;
  }
}

@keyframes shk-03a-head {
  20% {
    rotate: -6deg;
  }

  40% {
    rotate: 5deg;
  }

  60% {
    rotate: -3deg;
  }

  80% {
    rotate: 2deg;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-03a__row.is-wrong,
    .shk-03a__avatar.is-wrong {
    animation: none;
  }
}

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

.shk-03b {
  --err: oklch(0.68 0.21 25);
  --ok: oklch(0.78 0.16 160);
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #07080c;
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.shk-03b__phone {
  display: grid;
  justify-items: center;
  gap: 26px;
  padding: 44px 38px;
  border-radius: 34px;
  background: linear-gradient(180deg,#0e1018,#0a0c12);
  border: 1px solid rgba(160,180,220,.14);
  box-shadow: 0 60px 120px -60px rgba(0,0,0,1);
}

.shk-03b__label {
  font-size: 13.5px;
  color: rgba(225,232,248,.65);
  letter-spacing: .02em;
  min-height: 20px;
}

.shk-03b__label.err {
  color: oklch(0.8 0.13 25);
}

.shk-03b__label.ok {
  color: var(--ok);
}

.shk-03b__dots {
  display: flex;
  gap: 18px;
}

.shk-03b__dots i {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid rgba(225,232,248,.45);
  transition: background .15s,border-color .15s,scale .15s;
}

.shk-03b__dots i.fill {
  background: #e1e8f8;
  border-color: #e1e8f8;
  scale: 1.1;
}

.shk-03b__dots.wrong {
  animation: shk-03b-rattle .5s cubic-bezier(.36,.07,.19,.97);
}

.shk-03b__dots.wrong i {
  background: var(--err);
  border-color: var(--err);
}

.shk-03b__dots.right i {
  background: var(--ok);
  border-color: var(--ok);
}

.shk-03b__pad {
  display: grid;
  grid-template-columns: repeat(3,72px);
  gap: 14px;
}

.shk-03b__pad button {
  height: 72px;
  border-radius: 50%;
  border: 1px solid rgba(160,180,220,.16);
  background: rgba(255,255,255,.05);
  color: #e1e8f8;
  font: inherit;
  font-size: 24px;
  font-weight: 500;
  cursor: pointer;
  transition: background .12s,scale .12s;
}

.shk-03b__pad button:hover {
  background: rgba(255,255,255,.1);
}

.shk-03b__pad button:active {
  background: rgba(255,255,255,.22);
  scale: .94;
}

.shk-03b__pad [data-del] {
  font-size: 19px;
}

@keyframes shk-03b-rattle {
  15% {
    translate: -12px 0;
  }

  30% {
    translate: 10px 0;
  }

  45% {
    translate: -8px 0;
  }

  60% {
    translate: 6px 0;
  }

  75% {
    translate: -3px 0;
  }

  90% {
    translate: 1px 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .shk-03b__dots.wrong {
    animation: none;
  }
}
```

## JavaScript
```js
(function(){
  var form=document.getElementById('shk-03a-form'); if(!form) return;
  var row=form,input=document.getElementById('shk-03a-pass'),caps=document.getElementById('shk-03a-caps'),
      status=document.getElementById('shk-03a-status'),avatar=document.getElementById('shk-03a-avatar'),tries=0;
  function retrigger(el,cls){ el.classList.remove(cls); requestAnimationFrame(function(){ requestAnimationFrame(function(){ el.classList.add(cls); }); }); }
  row.addEventListener('animationend',function(){ row.classList.remove('is-wrong'); avatar.classList.remove('is-wrong'); });
  input.addEventListener('keyup',function(e){ if(e.getModifierState) caps.hidden=!e.getModifierState('CapsLock'); });
  form.addEventListener('submit',function(e){
    e.preventDefault();
    if(input.value==='demo123'){ status.className='shk-03a__status ok'; status.textContent='Welcome back, Kai.'; return; }
    tries++;
    retrigger(row,'is-wrong'); retrigger(avatar,'is-wrong');
    status.className='shk-03a__status err';
    status.textContent='Wrong password ('+tries+(tries===1?' try':' tries')+'). Hint: demo123';
    input.select();
  });
})();

(function(){
  var pad=document.getElementById('shk-03b-pad'); if(!pad) return;
  var dots=document.getElementById('shk-03b-dots'),label=document.getElementById('shk-03b-label'),
      pins=dots.querySelectorAll('i'),buf='',locked=false;
  function paint(){ pins.forEach(function(d,i){ d.classList.toggle('fill',i<buf.length); }); }
  dots.addEventListener('animationend',function(){
    dots.classList.remove('wrong'); buf=''; paint(); locked=false;   // clear AFTER the rattle
  });
  pad.addEventListener('click',function(e){
    var b=e.target.closest('button'); if(!b||locked) return;
    if(b.hasAttribute('data-del')){ buf=buf.slice(0,-1); paint(); return; }
    if(buf.length>=4) return;
    buf+=b.textContent.trim(); paint();
    if(buf.length<4) return;
    if(buf==='2468'){ dots.classList.add('right'); label.className='shk-03b__label ok'; label.textContent='Unlocked. Welcome back.'; locked=true; return; }
    locked=true; dots.classList.add('wrong');
    label.className='shk-03b__label err'; label.textContent='Wrong passcode — try 2468';
  });
})();
```

How this works

Both variants share one contract: JavaScript only judges the attempt and toggles a class; CSS owns every frame of motion.

field.classList.remove('is-wrong');  // reset mid-animation safely
requestAnimationFrame(() =>
  requestAnimationFrame(() => field.classList.add('is-wrong')));

The double requestAnimationFrame guarantees the class is really gone for one painted frame, so rapid wrong attempts each get a fresh shake without the old void-offsetWidth reflow hack. The PIN variant shakes the dot row rather than the whole card — motion stays where the user's eyes already are — and pairs it with a red flush, then clears the buffer on animationend so the interaction loop closes itself.

Make it yours

  • Keep password shakes fast (≈450ms) — unlock screens are retried immediately and a slow shake blocks the next attempt.
  • Shake only the field/dots, not the whole dialog, when the user will retry in place.
  • Localize the caps-lock hint — it's the highest-value string in any password UI.
  • For PIN pads, clear the buffer AFTER the shake ends (animationend), never before — users need to see what they typed rattle.

Gotchas — read before shipping

  • Never validate real passwords client-side — these demos gate a fake secret; in production the class toggle happens in the fetch().catch()/error branch.
  • getModifierState('CapsLock') needs a real key event; check it on keydown/keyup, not on focus.
  • Don't clear the field synchronously with the shake — the visual 'my input got rejected' link breaks.
  • Screen-reader users hear nothing from a shake: write the failure into an aria-live region every attempt.

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

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

Everything here is baseline: class toggles, transform keyframes, rAF. getModifierState is supported in all evergreen browsers; when unavailable the caps-lock hint simply never shows.

Techniques used in this demo

Search CodeFronts

Loading…