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.

checkValidity versus reportValidity side by side Both methods read the same ValidityState. checkValidity returns a boolean with no UI. reportValidity returns the same boolean and additionally shows a tooltip, scrolls, and focuses the first invalid field. ValidityState same flags read checkValidity() returns boolean — silent reportValidity() returns boolean no tooltip no scroll, no focus you sync ARIA yourself shows native tooltip scrolls + focuses first invalid fires invalid event
Identical boolean, divergent side effects: 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

  1. In DevTools, set a required field empty and run $0.checkValidity() in the console — it returns false with no popup. Then run $0.reportValidity() — the tooltip appears and focus jumps to the field.
  2. Confirm the invalid event fires for both: $0.addEventListener('invalid', () => console.log('fired')), then call each method.
  3. Add novalidate to the form and re-run form.reportValidity() — it returns true and 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.

← Back to Constraint Validation API Deep Dive