Input shake

Free · MIT

Validation wrapper for any input that shakes and slides in an error with an icon when invalid, and draws a check circle when valid.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/input-shake.json

The file lands in components/goodui/input-shake.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/input-shake works too.

Usage

import * as React from "react"
import { Input } from "@/components/ui/input"
import { InputShake, type InputShakeStatus } from "@/components/goodui/input-shake"

const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/

export function WorkEmailField() {
  const [email, setEmail] = React.useState("")
  const [touched, setTouched] = React.useState(false)
  const [attempt, setAttempt] = React.useState(0)
  const error = !email ? "Enter your work email" : !EMAIL.test(email) ? "That email looks incomplete" : null
  const status: InputShakeStatus = !touched ? "idle" : error ? "invalid" : "valid"

  return (
    <form
      noValidate
      className="grid gap-3"
      onSubmit={(e) => {
        e.preventDefault()
        setTouched(true)
        setAttempt((a) => a + 1)
      }}
    >
      <InputShake label="Work email" status={status} message={error} successMessage="Looks good" shakeKey={attempt}>
        <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} onBlur={() => setTouched(true)} />
      </InputShake>
      <button type="submit">Create workspace</button>
    </form>
  )
}

When to use

Use it for
  • Sign-up and login forms where a failed submit should be impossible to miss.
  • Wrapping inputs you already have (shadcn Input, a native <select>) to add an error, a success check and a label without rebuilding them.
  • Server-side errors ("This email already has a workspace") that should shake the field again on each failed submit.
Reach for something else
  • Errors while the user is still typing: show them on blur or submit; a shake on every keystroke is noise.
  • Building a field from scratch: floating-label-input already has its own error slot.
  • One-time codes: otp-input has its own error shake and success bounce.

Examples

Native select with a hint

<InputShake label="Plan" status={plan ? "valid" : touched ? "invalid" : "idle"} message="Pick a plan" hint="You can change it later.">
  <select className="h-9 w-full rounded-md border px-3" value={plan} onChange={(e) => setPlan(e.target.value)}>
    <option value="">Choose…</option>
    <option value="starter">Starter, $19/mo</option>
    <option value="pro">Pro, $49/mo</option>
  </select>
</InputShake>

API reference

PropTypeDefault
children*
A single input-like element (native input, shadcn Input, select…).
React.ReactElement<ChildProps>—
status
"invalid" shakes the field (on entry) and shows `message`; "valid" draws a check and shows `successMessage`.
"idle" | "invalid" | "valid""idle"
message
Error text shown while invalid.
React.ReactNode—
successMessage
Optional text shown while valid.
React.ReactNode—
shakeKey
Change this (e.g. a submit counter) to shake again while the field is still invalid.
number | string—
label
Visible label rendered as a <label> linked to the child's id.
React.ReactNode—
hint
Helper text shown when there is no message.
React.ReactNode—

Also accepts every <div> attribute.

Accessibility

  • label renders a <label> tied to the child's id (the child's own id is kept, or one is generated).
  • The message (or hint) is linked to the child with aria-describedby, appended to any aria-describedby the child already has.
  • While invalid the child gets aria-invalid and the error has role="alert", so it is read out when it appears. The success message has role="status", so it is announced politely.
  • The error and check icons are decorative (aria-hidden); the text carries the meaning.

Motion

  • Becoming invalid shakes the field sideways for 0.42 s; changing shakeKey while still invalid shakes it again.
  • The error icon spins in from -45° and scale 0.4 on a spring (stiffness 600, damping 22); the valid check draws its circle in 0.35 s and its tick in 0.25 s after a 0.18 s delay.
  • The message slides down and grows to its height in 0.2 s; the hint only fades.
  • Reduced motion: no shake, the icons and message only fade, and the check appears already drawn.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • Pass exactly one input-like element as children; it must accept id, className, aria-invalid and aria-describedby.
  • The component does not validate. Compute status ("idle" | "invalid" | "valid") and message yourself.
  • To shake on every failed submit, pass a counter as shakeKey and increment it in onSubmit.
  • It adds pr-9 to the child for the icon; do not add your own icon on the right side.
  • Keep status "idle" until the field is touched or submitted, so new forms do not open full of errors.

FAQ

Does it work with react-hook-form?

Yes. Map fieldState.error to status="invalid" and message, and pass formState.submitCount as shakeKey.

Why does it not shake on every keystroke?

It shakes when status changes to invalid, or when shakeKey changes while invalid. Staying invalid while typing does not shake again.

Related