# useInvalidShake

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

Docs: https://hextaui.com/docs/use-invalid-shake
Markdown: https://hextaui.com/docs/use-invalid-shake.md

```tsx title="components/examples/use-invalid-shake/demo.tsx"
"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>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/use-invalid-shake.json
```

This adds the hook and anything it depends on.

### Manual

Copy and paste the following code into your project.

```ts title="hooks/use-invalid-shake.ts"
import * as React from "react"

import { prefersReducedMotion } from "@/lib/motion"

type ShakeControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement

const submitSelector =
  'button:not([type]), button[type="submit"], input[type="submit"], input[type="image"]'

function isInvalid(control: ShakeControl) {
  return (
    control.getAttribute("aria-invalid") === "true" ||
    control.hasAttribute("data-invalid") ||
    control.matches(":user-invalid")
  )
}

function useInvalidShake(
  ref: React.RefObject<ShakeControl | null>,
  enabled = true
) {
  React.useEffect(() => {
    const control = ref.current
    const form = control?.form
    if (!enabled || !control || !form) {
      return
    }

    let attempt = 0
    let shaken = 0
    let attemptedAt = -Infinity
    let timer: ReturnType<typeof setTimeout> | undefined
    let frame = 0

    const shake = () => {
      if (shaken === attempt || prefersReducedMotion()) {
        return
      }
      shaken = attempt
      const target =
        control.closest<HTMLElement>("[data-slot=input-group]") ?? control
      target.removeAttribute("data-shake")
      void target.offsetWidth
      target.setAttribute("data-shake", "")
      clearTimeout(timer)
      timer = setTimeout(() => target.removeAttribute("data-shake"), 400)
    }

    const recent = () => performance.now() - attemptedAt < 600

    const check = () => {
      if (recent() && isInvalid(control)) {
        shake()
      }
    }

    const onAttempt = () => {
      attempt += 1
      attemptedAt = performance.now()
      cancelAnimationFrame(frame)
      frame = requestAnimationFrame(check)
    }

    const onClick = (event: MouseEvent) => {
      const target = event.target as Element | null
      if (target?.closest(submitSelector)) {
        onAttempt()
      }
    }

    const onKeyDown = (event: KeyboardEvent) => {
      const target = event.target as Element | null
      if (event.key === "Enter" && target instanceof HTMLInputElement) {
        onAttempt()
      }
    }

    const onInvalid = () => {
      if (recent()) {
        shake()
      }
    }

    const attributes = new MutationObserver(check)
    attributes.observe(control, {
      attributes: true,
      attributeFilter: ["aria-invalid", "data-invalid"],
    })

    form.addEventListener("click", onClick, true)
    form.addEventListener("keydown", onKeyDown, true)
    form.addEventListener("submit", onAttempt, true)
    control.addEventListener("invalid", onInvalid)

    return () => {
      attributes.disconnect()
      form.removeEventListener("click", onClick, true)
      form.removeEventListener("keydown", onKeyDown, true)
      form.removeEventListener("submit", onAttempt, true)
      control.removeEventListener("invalid", onInvalid)
      cancelAnimationFrame(frame)
      clearTimeout(timer)
    }
  }, [ref, enabled])
}

export { useInvalidShake }
export type { ShakeControl }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { useInvalidShake } from "@/hooks/use-invalid-shake"
```

```tsx
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.

## When it shakes

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.

| Event | Description |
| --- | --- |
| `Submit attempt` | Clicking a submit button, pressing Enter in an input of the form, or the form's submit event. |
| `Invalid within 600ms` | The 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. |
| `Shake` | data-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.

## Good to know

- 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.

## API reference

### useInvalidShake(ref, enabled?)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `ref` | `RefObject<HTMLInputElement \| HTMLTextAreaElement \| HTMLSelectElement \| null>` | – | The form control to watch. |
| `enabled` | `boolean` | `true` | Whether to shake. |

| Attribute | Description |
| --- | --- |
| `data-shake` | Present for 400ms after a failed submit attempt, on the control or its InputGroup. |

### Used by

`Input`, `Textarea`, `NativeSelect` and `InputGroup`.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
