DEV Community

Petteri Pucilowski
Petteri Pucilowski

Posted on

Two checks worth adding when you rewrite a checkout modal

While rebuilding the free-preview and checkout flow in CrawlGraph, our review caught two details that are easy to miss: inherited disabled state and native email validation.

The browser APIs covered both cases. We added checks for the pending request state.

I build CrawlGraph, a backlink research and outreach tool. Here are the two checks from the interface work we just released.

1. A fieldset can disable an input without changing its attributes

Our checkout has a plan chooser. While its request is pending, the chooser's fieldset is disabled.

A focus-trap selector like this looks reasonable:

panel.querySelectorAll('button:not([disabled]), input:not([disabled])');
Enter fullscreen mode Exit fullscreen mode

But an input inside a disabled fieldset can match that selector while still being disabled by the browser. It may have no disabled attribute of its own.

That matters when your trap treats the input as its last focusable element. Shift+Tab can try to focus a control that cannot receive focus. Forward Tab can leave the dialog because your calculated boundary is wrong.

Check effective state as well as visibility:

const controls = [...panel.querySelectorAll(focusableSelector)]
  .filter((element) => element.getClientRects().length > 0)
  .filter((element) => !element.matches(':disabled'));
Enter fullscreen mode Exit fullscreen mode

Use a delayed checkout response, then press Tab and Shift+Tab from the close button while the request is pending.

MDN describes how disabled fieldsets affect their descendant controls.

2. Inline errors should keep the browser's email validity check

We wanted an inline error instead of the browser's validation bubble, so the rewritten form used noValidate.

That makes the submit handler responsible for validation. A check that only asks whether the string contains @ accepts user@@example.com far enough to start a request.

For an email input, the browser still exposes its validity state:

const invalid =
  !email.trim() ||
  input.validity.typeMismatch ||
  !input.validity.valid;

if (invalid) {
  showInlineError();
  input.focus();
  return;
}

// Track the valid action and start the request after this guard.
Enter fullscreen mode Exit fullscreen mode

Keep the visible label, associate the error with the field, and preserve the typed address. Client validation improves the interaction; the server still validates the request independently.

The typeMismatch documentation explains the email and URL checks.

One more check for nested dialogs

Our free preview can open checkout. Escape should close the top dialog, preserve the preview beneath it, and return focus to the button that opened checkout. Background scrolling should remain locked until the final dialog closes.

We now render one responsive panel per modal instead of separate desktop and mobile copies. A shared stack owns focus and scroll behavior.

The WAI modal-dialog pattern is a useful baseline for Tab, Shift+Tab, initial focus and Escape behavior. We checked the Stripe backlink lookup against the live API and covered pending, error and already-paid checkout states with controlled responses.

The updated CrawlGraph preview lets you try up to five referring-domain rows without creating an account. Paid retrieval and CSV/JSON exports support up to 100,000 referring-domain rows per lookup. Paid users can also save outreach targets, find published contact emails and open a draft in their mail app.

The updated flow is live. Try a stripe.com lookup and follow the results into the outreach workflow.

Top comments (0)