Interaction blueprint

Inline validation shake

A short horizontal nudge calls attention once, while the message explains the problem.

Playground

Playhead 0ms / 320ms

Build it

Choose a target and adapt the source to your interface.

119 lines
index.html
<form class="validation-form" novalidate>
  <label for="email">Email</label
  ><input
    id="email"
    name="email"
    type="email"
    autocomplete="email"
    required
    aria-describedby="email-error"
  />
  <p id="email-error" class="validation-message" role="alert"></p>
  <button type="submit">
    Check email<svg viewBox="0 0 24 24" aria-hidden="true">
      <path d="M5 12h14m-5-5 5 5-5 5" />
    </svg>
  </button>
  <p class="validation-status" role="status"></p>
</form>

<script>
  const form = document.querySelector('.validation-form');
  const input = form.querySelector('input');
  const error = form.querySelector('.validation-message');
  const status = form.querySelector('.validation-status');
  let animation;
  input.addEventListener('input', () => {
    input.removeAttribute('aria-invalid');
    error.textContent = '';
    status.textContent = '';
  });
  form.addEventListener('submit', (event) => {
    event.preventDefault();
    animation?.cancel();
    const valid = input.validity.valid;
    input.setAttribute('aria-invalid', String(!valid));
    error.textContent = valid ? '' : 'Enter a valid email address.';
    status.textContent = valid ? 'Email format accepted.' : '';
    if (!valid) {
      input.focus();
      if (!matchMedia('(prefers-reduced-motion:reduce)').matches)
        animation = input.animate(
          [0, -6, 5, -3, 2, 0].map((x) => ({
            transform: 'translateX(' + x + 'px)',
          })),
          { duration: 320 },
        );
    }
  });
</script>

<style>
  .validation-form {
    display: grid;
    gap: 8px;
    max-width: 320px;
    font: 14px system-ui;
    color: var(--stage-ink, var(--ink, #252b34));
  }
  .validation-form input {
    min-height: 44px;
    padding: 10px 12px;
    box-sizing: border-box;
  }
  .validation-form button {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 28px;
    justify-self: start;
    min-height: 44px;
    padding: 0 16px 0 18px;
    border: 0;
    border-radius: 999px;
    background: var(--stage-ink, var(--ink, #252b34));
    color: var(--stage-bg, var(--bg, #f8f9fb));
    font: 500 13px/1 system-ui;
    cursor: pointer;
  }
  .validation-form button svg {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .validation-form button:hover svg {
    transform: translateX(3px);
  }
  .validation-form button:active svg {
    transform: translateX(1px);
  }
  .validation-form button:focus-visible {
    outline: 2px solid var(--stage-link, var(--brand-deep, #175bc5));
    outline-offset: 3px;
  }
  .validation-form input {
    border: 1px solid #68778c;
    border-radius: 4px;
  }
  .validation-form input[aria-invalid='true'] {
    border-color: #b42318;
  }
  .validation-message {
    min-height: 20px;
    margin: 0;
    color: #b42318;
  }
  @media (prefers-reduced-motion: reduce) {
    .validation-form input {
      animation: none;
    }
    .validation-form button svg {
      transition: none;
    }
  }
</style>

Animation breakdown

Frame phases, motion intent, implementation constraints and accessible alternatives.

320ms
01

Rest: invalid field and error text.

02

0–220ms: field moves -6, +5, -3, +2px.

03

220–320ms: return to centre; error remains.

Motion intent

A short horizontal nudge calls attention once, while the message explains the problem.

This duration leaves enough time to read the state change while keeping the interface responsive.

The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.

Motion specification

Trigger
A submitted field fails validation.
Timing
320ms
Easing
cubic-bezier(.36, .07, .19, .97)

Animated properties

transformopacity

Implementation notes

  • Keep invalid and announced aligned to the same visual anchor.
  • Drive the transition from semantic state, not a timer, when the trigger is interactive.
  • Use transform, opacity and avoid layout work inside the animation loop.

Reduced motion

Swap to the final state instantly, preserving colour, label, and accessible state.

  • Use a minimum 44 by 44 pixel interactive target.
  • Expose the invalid and announced states with aria-pressed, aria-expanded, or an updated accessible label.
  • Keep visible keyboard focus independent from the animation.

Avoid

  • Do not add bounce unless a real object pickup or spring settle is part of the interaction.
  • Do not hide the final state from assistive technology.
  • Do not make the user wait for animation before the underlying state updates.

Related blueprints

Browse all blueprints