Focus & scroll on invalid submit

The default does the right thing on submit. The imperative helpers exist for when you need to drive focus or scroll outside the submit path.

Category
Return methods
Auto behavior
handleSubmit on invalid → focusFirstError
Helpers
focusFirstError(options?) · scrollToFirstError(options?)
Returns
boolean; true if a target was found

Submit the form with empty fields to watch focus pull to the first invalid one automatically. That's handleSubmit running its default invalid-submit policy ('focus-first-error'). The two buttons below dispatch the helpers imperatively, so you can drive focus or smooth-scroll outside the submit handler. Submitting again with valid fields shows the no-op success path.

Focus & Scroll Demo Open in playground

Submit with empty form.fields to see focus + scroll pull to the first invalid path. Click the buttons to dispatch each helper imperatively.

Default on invalid submit

handleSubmit pulls focus to the first invalid field on failed submission:

const onSubmit = form.handleSubmit(async (values) => {
  await api.send(values)
})

When validation fails, the handler:

  1. Increments form.meta.submissionAttempts. form.meta.submitted stays false; it only flips on a successful callback.
  2. Surfaces errors at every invalid path.
  3. Calls form.focusFirstError() (the same method exposed below).
  4. Calls onError(errors) if you passed one.

The "first" invalid field is in schema-declaration order, which matches the visual reading order for most forms (top to bottom, left to right).

focusFirstError(options?)

form.focusFirstError({ preventScroll: false })

Returns true when a target was found and focused, false when no field is in an error state. The optional preventScroll: true skips the browser's default focus-related scroll if you've got a custom scroll strategy.

Reach for this when:

  • A page-level error banner has a "Jump to first error" button.
  • A multi-step form's "Next" button should pull focus on validation failure without going through handleSubmit.
  • Replacing the default invalid-submit policy with custom UX (see Customizing the invalid-submit policy).

scrollToFirstError(options?)

form.scrollToFirstError({ behavior: 'smooth', block: 'center' })

Returns true when a target was found and scrolled into view. Options forward to the underlying Element.scrollIntoView: behavior: 'smooth' for animated scroll, block: 'center' to position the field in the middle of the viewport.

The default invalid-submit policy focuses but doesn't scroll on most browsers (focus triggers a minimal scroll). For tall forms where the first error might be far above the user's current scroll position, layer this on:

const onSubmit = form.handleSubmit(
  async (values) => {
    /* ... */
  },
  () => {
    form.scrollToFirstError({ behavior: 'smooth', block: 'center' })
  }
)

Customizing the invalid-submit policy

Disable the default focus pull at the form level and run your own:

useForm({
  schema,
  onInvalidSubmit: 'none', // skip the default focus
})

Then drive focus + scroll from onError:

const onSubmit = form.handleSubmit(onSubmitValid, () => {
  form.scrollToFirstError({ behavior: 'smooth', block: 'center' })
  form.focusFirstError({ preventScroll: true })
})

The 'focus-first-error' (default), 'scroll-to-first-error', 'both', and 'none' policy options live on the form config and on createAttaform({ defaults }) for an app-wide default. See the Types reference for the full set.

Where to next