HextaUI

useInvalidShake

Shakes a form control when a submit attempt finds it invalid, and never while someone is still typing.

Submit without choosing.
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { useInvalidShake } from "@/hooks/use-invalid-shake"

export function UseInvalidShakeDemo() {
  const ref = React.useRef<HTMLSelectElement>(null)
  const [sent, setSent] = React.useState<string>()
  useInvalidShake(ref)

  return (
    <form
      className="flex w-full max-w-xs flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        setSent(String(new FormData(event.currentTarget).get("plan")))
      }}
    >
      <label className="flex flex-col gap-2 text-sm font-medium">
        Plan
        <select
          ref={ref}
          name="plan"
          required
          defaultValue=""
          className="h-9 rounded-md bg-muted px-2.5 text-sm font-normal outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden data-shake:motion-safe:animate-button-shake pointer-coarse:text-touch [&:user-invalid]:ring-2 [&:user-invalid]:ring-destructive/40"
        >
          <option value="" disabled>
            Choose a plan
          </option>
          <option value="hobby">Hobby</option>
          <option value="pro">Pro</option>
        </select>
      </label>
      <Button type="submit" size="sm">
        Continue
      </Button>
      <output className="text-sm text-muted-foreground">
        {sent ? `plan=${sent}` : "Submit without choosing."}
      </output>
    </form>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/use-invalid-shake.json

Adds the hook and anything it depends on to your project.

import { useInvalidShake } from "@/hooks/use-invalid-shake"
const ref = React.useRef<HTMLSelectElement>(null)
useInvalidShake(ref)

<form>
  <select
    ref={ref}
    required
    className="data-shake:motion-safe:animate-button-shake"
  >
    …
  </select>
  <button type="submit">Continue</button>
</form>

The hook only sets a data-shake attribute. The motion comes from the theme's animate-button-shake, which you attach with data-shake:motion-safe:animate-button-shake. HextaUI's Input, Textarea, Native select and Input group already include that class and call the hook. Use it directly for your own controls.

A shake answers one question: why didn't my submit work? So the hook only shakes right after someone tries to submit, and never while they're still filling in the form.

EventDescription
Submit attemptClicking a submit button, pressing Enter in an input of the form, or the form's submit event.
Invalid within 600msThe control fires invalid, or has aria-invalid="true", data-invalid or :user-invalid. That covers native validation, Base UI fields and server errors set right after submitting.
Shakedata-shake is set for 400ms. It shakes once per attempt, however many of those signals arrive.
  • Inside an InputGroup, the whole group shakes, not just the inner control.
  • Under reduced motion, nothing is set. Pair the shake with a visible error message either way, since the shake is a cue, not the message.
  • The hook needs the control to belong to a <form>. Without one, it does nothing.
  • Turn it off per control with the enabled argument, or with shake={false} on HextaUI's inputs.
  • Restarting the attribute restarts the animation, so a second failed attempt shakes again even if the first is still running.
  • Native validation focuses the first invalid control on submit. The shake only adds motion on top of that and never moves focus.
PropTypeDefault
refThe form control to watch.
RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null>–
enabledWhether to shake.
booleantrue
AttributeDescription
data-shakePresent for 400ms after a failed submit attempt, on the control or its InputGroup.

Input, Textarea, NativeSelect and InputGroup.