checkValidity vs reportValidity: Core Behavioral Differences
checkValidity() and reportValidity() return the identical boolean from the identical ValidityState evaluation, but only reportValidity() produces side effects — native error tooltips, viewport scroll, and focus on the first invalid control — which is exactly why choosing the wrong one causes either silent failures or aggressive focus-stealing.
When to Use This Recipe
Reach for this decision when you are wiring up a form’s submit handler, a real-time field check, or a multi-step “Next” gate and need to decide which method to call. The rule is short: check silently as often as you like; report only in response to an explicit user action.
| Requirement | Method |
|---|---|
| Programmatic state checks, analytics, custom UI sync | checkValidity() |
| Debounced real-time field validation | checkValidity() |
| Per-step “Next” gate in a wizard | checkValidity() first, reportValidity() on failure |
| Final submission gate or explicit “Validate” button | reportValidity() |
| Showing native browser tooltips and focus | reportValidity() |
Both methods belong to the broader Constraint Validation API Deep Dive, and both are foundational to Mastering HTML5 Native Form Validation. Neither method enforces validation — they only read the flags that your required, pattern, type, and setCustomValidity() constraints have already populated.
reportValidity() owns the visible UI and focus.How checkValidity() Works: Silent State Evaluation
checkValidity() synchronously evaluates every constraint attribute and returns a boolean. It dispatches the invalid event on each failing control (per spec) but deliberately bypasses the rendering pipeline, so there is no tooltip and no focus change — ideal for background logic, debounced input checks, and syncing your own accessible UI.
const input = document.querySelector<HTMLInputElement>('#email');
if (input && !input.checkValidity()) {
// Safe to log, track, and style without any visual side effects.
console.warn('Validation failed:', input.validationMessage);
input.classList.add('is-invalid');
// Native UI is absent, so you must sync ARIA manually:
input.setAttribute('aria-invalid', 'true');
input.setAttribute('aria-describedby', `${input.id}-error`);
}
Key behaviors: it fires invalid but shows no UI and moves no focus; on a <form> it returns true immediately if the form carries novalidate; it returns true for disabled or readonly candidates; and it requires manual aria-invalid/aria-describedby updates for screen reader support.
How reportValidity() Works: UI Feedback and Focus
reportValidity() returns the same boolean and then renders the browser’s native validation UI: the tooltip, a scroll into view, focus on the first invalid field, and the invalid event. It integrates cleanly with the canonical novalidate submit gate.
const form = document.querySelector<HTMLFormElement>('#signup-form');
if (form) {
form.addEventListener('submit', (event: SubmitEvent) => {
event.preventDefault();
// Silent gate first; escalate to native UI only on this submit action.
if (!form.checkValidity()) {
form.reportValidity(); // Tooltip + scroll + focus first invalid.
return;
}
submitFormData(form);
});
}
Key behaviors: it respects novalidate (returns true with no UI when present); Safari may delay or suppress tooltips on transformed or fixed-position inputs; and invoking it on rapid input/keydown events steals focus and disrupts typing.
Parameter and Behavior Reference
| Aspect | checkValidity() |
reportValidity() |
|---|---|---|
| Return value | boolean |
boolean (identical) |
Fires invalid event |
Yes | Yes |
| Shows native tooltip | No | Yes |
| Scrolls to first invalid | No | Yes |
| Moves focus | No | Yes |
Honors novalidate |
Yes (returns true) |
Yes (returns true) |
| Safe to call per keystroke | Yes (debounce anyway) | No — steals focus |
| Requires manual ARIA sync | Yes | Partially (browser announces) |
Verification Steps
- In DevTools, set a required field empty and run
$0.checkValidity()in the console — it returnsfalsewith no popup. Then run$0.reportValidity()— the tooltip appears and focus jumps to the field. - Confirm the
invalidevent fires for both:$0.addEventListener('invalid', () => console.log('fired')), then call each method. - Add
novalidateto the form and re-runform.reportValidity()— it returnstrueand shows nothing, proving the attribute short-circuits both.
import { test, expect } from '@playwright/test';
test('reportValidity focuses the first invalid field; checkValidity does not', async ({ page }) => {
await page.goto('/signup');
await page.locator('#submit').click(); // submit handler calls reportValidity
await expect(page.locator('#email')).toBeFocused();
});
Edge Cases and Failure Modes
Chaining reportValidity() on every keystroke. This forces synchronous layout, steals focus mid-typing, and flickers the tooltip. Fix: debounce input and call checkValidity() there; reserve reportValidity() for blur and submit.
// ❌ Focus-stealing on every keystroke.
input.addEventListener('input', () => input.reportValidity());
// ✅ Silent check while typing; report only on blur.
input.addEventListener('input', () => input.checkValidity());
input.addEventListener('blur', () => input.reportValidity());
Stale custom validity causing divergence. If checkValidity() and reportValidity() disagree, a leftover setCustomValidity() string is almost always the cause — the browser caches it until cleared. Always reset with setCustomValidity('') before re-evaluating, as detailed in how to use setCustomValidity correctly.
Shadow DOM boundaries. reportValidity() may not surface UI for controls inside a shadowRoot. Call it on the host or scope form.elements to the light DOM, mirroring the cross-boundary notes in the Constraint Validation API Deep Dive.
Frequently Asked Questions
If they return the same boolean, why not always use reportValidity()?
Because reportValidity() always shows native UI and moves focus. Calling it during background checks or on every keystroke produces flickering tooltips and stolen focus. Use checkValidity() whenever you only need the boolean, and let reportValidity() run on deliberate user actions like submit.
Does either method work if the form has novalidate?
novalidate only suppresses the browser's automatic blocking on submit. Calling form.checkValidity() or form.reportValidity() explicitly still evaluates every constraint. Note that reportValidity() on a novalidate form returns true without UI, so call it on individual fields if you want the native tooltip while keeping novalidate on the form.
Why does checkValidity() pass but reportValidity() show an error?
They read identical state, so genuine divergence points to a stale setCustomValidity() string set on one element but not cleared, or a scoping mismatch between form.elements and a querySelectorAll. Reset every custom message with an empty string before re-running validation and confirm both calls target the same set of controls.
Related Guides
- Constraint Validation API Deep Dive — the full API these two methods belong to.
- Reading ValidityState Flags for Granular Errors — turn the boolean into a specific message.
- How to Use setCustomValidity Correctly — the leading cause of method divergence.
- Prevent Default Form Submission Without Losing Validation — where the submit gate calls these methods.