Description of the bug
When a Dialog containing a form is closed, FocusScope restores focus to the trigger element by
calling focus(previouslyFocusedElement). As a browser side-effect, calling .focus() on the
trigger fires a blur event on whichever input was last focused inside the dialog.
Libraries like vee-validate and FormKit validate on blur by default. This causes validation error
messages to flash on form fields as the dialog closes — even when the user never interacted with
those fields or intentionally dismissed the dialog.
Reproduction
validateOnBlur: true, which is the default)
2. Click into the input so it has focus
3. Press Escape or click the close button
4. Observe: the validation error appears on the input as the dialog closes
Root cause
In FocusScope.vue, the cleanupFn of the second watchEffect:
setTimeout(() => {
if (!unmountEvent.defaultPrevented)
focus(previouslyFocusedElement ?? document.body, { select: true })
...
}, 0)
Calling focus(trigger) causes the browser to synchronously fire blur on the currently-focused input.
There is no explicit .blur() call — this is standard browser behavior. reka-ui is not doing
anything wrong; the issue is that there is no signal to distinguish a system-initiated blur (focus
trap cleanup) from a user-initiated blur (tabbing away, clicking elsewhere).
The existing @unmount-auto-focus escape hatch does not help here because the blur fires
synchronously inside the setTimeout callback, after the event has already been dispatched.
Proposed solution
Add a data-focus-scope-unmounting attribute to the container for the duration of the focus
restoration. Consumers can check for this attribute in their blur handlers to skip validation:
// vee-validate custom integration
function handleBlur(event: FocusEvent) {
if ((event.target as Element).closest('[data-focus-scope-unmounting]')) return
// ... run validation
}
The attribute is set before the setTimeout, so it is present when blur fires synchronously inside
it. It is removed after focus() returns, so it does not linger.
Workaround (until fixed upstream)
Disable blur-triggered validation on dialog forms:
// vee-validate
const { handleSubmit } = useForm({ validateOnBlur: false })
See linked PR for the minimal 4-line change.
Description of the bug
When a Dialog containing a form is closed,
FocusScoperestores focus to the trigger element bycalling
focus(previouslyFocusedElement). As a browser side-effect, calling.focus()on thetrigger fires a
blurevent on whichever input was last focused inside the dialog.Libraries like vee-validate and FormKit validate on
blurby default. This causes validation errormessages to flash on form fields as the dialog closes — even when the user never interacted with
those fields or intentionally dismissed the dialog.
Reproduction
validateOnBlur: true, which is the default)2. Click into the input so it has focus
3. Press Escape or click the close button
4. Observe: the validation error appears on the input as the dialog closes
Root cause
In
FocusScope.vue, thecleanupFnof the secondwatchEffect:Calling focus(trigger) causes the browser to synchronously fire blur on the currently-focused input.
There is no explicit .blur() call — this is standard browser behavior. reka-ui is not doing
anything wrong; the issue is that there is no signal to distinguish a system-initiated blur (focus
trap cleanup) from a user-initiated blur (tabbing away, clicking elsewhere).
The existing @unmount-auto-focus escape hatch does not help here because the blur fires
synchronously inside the setTimeout callback, after the event has already been dispatched.
Proposed solution
Add a data-focus-scope-unmounting attribute to the container for the duration of the focus
restoration. Consumers can check for this attribute in their blur handlers to skip validation:
The attribute is set before the setTimeout, so it is present when blur fires synchronously inside
it. It is removed after focus() returns, so it does not linger.
Workaround (until fixed upstream)
Disable blur-triggered validation on dialog forms:
See linked PR for the minimal 4-line change.