Shake on Error

Submitting an invalid code makes the field shake side to side and show the error underneath, like a head saying no.

  • CSS
  • Forms & controls
  • Click
  • Added

AI prompt

Create a React form field that shakes when validation fails. On submit, check the value against /^TWL-\d{4}$/i. If it fails, set aria-invalid="true", show the message "That code is not valid. Check the email we sent you." in an element referenced by aria-describedby with role="alert", and play a CSS keyframe on the field: translateX 0, -8px, 7px, -5px, 3px, 0 over 400ms with cubic-bezier(0.65, 0, 0.35, 1). Restart the shake on every failed submit by toggling a key or removing and re-adding the class on animationend. Keep focus in the field. If it passes, clear the error and show "You are in.". Under prefers-reduced-motion, skip the shake and only change the border colour and show the message.

Source

Demo.tsx
'use client'

import { useEffect, useId, useRef, useState } from 'react'
import s from './demo.module.css'

export default function ShakeError({ labels }: { labels: Record<string, string> }) {
  const [status, setStatus] = useState<'idle' | 'error' | 'ok'>('idle')
  const [attempt, setAttempt] = useState(0)
  const [value, setValue] = useState('TWL-12')
  const input = useRef<HTMLInputElement>(null)
  const id = useId()

  // the input remounts (key) to restart the shake, so focus the new element afterwards
  useEffect(() => {
    if (attempt) input.current?.focus()
  }, [attempt])

  const submit = (e: React.FormEvent) => {
    e.preventDefault()
    const ok = /^TWL-\d{4}$/i.test(value.trim())
    setStatus(ok ? 'ok' : 'error')
    if (!ok) setAttempt(attempt + 1)
  }

  return (
    <form className={s.form} onSubmit={submit} noValidate>
      <label htmlFor={id} className={s.label}>
        {labels.label}
      </label>
      <div className={s.row}>
        <input
          key={attempt}
          ref={input}
          id={id}
          className={s.input}
          placeholder={labels.placeholder}
          aria-invalid={status === 'error'}
          aria-describedby={`${id}-msg`}
          value={value}
          onChange={(e) => setValue(e.target.value)}
          autoComplete="off"
        />
        <button type="submit" className={s.button}>
          {labels.submit}
        </button>
      </div>
      <p id={`${id}-msg`} className={s.msg} data-status={status} role="alert">
        {status === 'error' ? labels.error : status === 'ok' ? labels.ok : ''}
      </p>
    </form>
  )
}
demo.module.css
.form { display: grid; gap: 8px; width: min(300px, 100%); }

.label { color: var(--stage-muted); font: 500 var(--text-sm) / 1 var(--font-body); }

.row { display: flex; gap: 8px; }

.input {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: 10px;
  outline: none;
  background: var(--stage-bg);
  color: var(--stage-ink);
  font: 500 var(--text-base) / 1 var(--font-mono);
}

.input:focus { border-color: var(--stage-muted); }
.input:focus-visible { box-shadow: 0 0 0 3px var(--color-focus); }
.input[aria-invalid='true'] { border-color: var(--color-error); animation: shake 400ms var(--ease-in-out); }

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); }
  40% { transform: translateX(7px); }
  60% { transform: translateX(-5px); }
  80% { transform: translateX(3px); }
}

.button {
  height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 10px;
  background: var(--stage-ink);
  color: var(--stage-bg);
  font: 600 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:hover { background: var(--stage-accent); color: var(--stage-accent-ink); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.msg { min-height: 2.6em; margin: 0; font-size: var(--text-xs); line-height: 1.3; }
.msg[data-status='error'] { color: var(--color-error); }
.msg[data-status='ok'] { color: var(--stage-accent); }

@media (prefers-reduced-motion: reduce) {
  .input[aria-invalid='true'] { animation: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.