Suggesting Email Domain Typo Corrections
How do you catch ada@gmial.com, ada@hotmail.con and ada@yahooo.com before a user signs up with an address that will never receive their confirmation email — without rejecting the perfectly valid ada@gmx.com or a company domain you have never heard of? The answer is to suggest, never to block. This recipe keeps the email field’s validity with the browser’s type="email" check and the Constraint Validation API, and separately compares the domain against a list of popular mail providers using edit distance. When a close match exists, it offers a one-click “Did you mean ada@gmail.com?” button that is announced once and returns focus to the field.
When to Offer Email Typo Suggestions
Domain typos are one of the most common reasons sign-up confirmation emails never arrive, and they are invisible to syntax validation: ada@gmial.com is a perfectly valid address that happens to belong to nobody. Offer suggestions when:
- Most users have consumer email, so a short list of providers covers most addresses.
- The address is critical — account recovery, order receipts, double opt-in newsletters.
- You cannot easily correct it later, because the user will never see the confirmation.
Never turn a suggestion into an error. Plenty of real domains sit one edit away from a popular one (gmx.com, mail.com, ymail.com), and blocking them locks out real customers. The syntax check stays authoritative, exactly as described in email input validation attributes; the suggestion is advice layered on top, in the “likelihood” tier of the identity text field validation topic.
Minimal Working Suggestion Engine
const PROVIDERS = [
"gmail.com", "googlemail.com", "yahoo.com", "yahoo.co.uk", "hotmail.com", "hotmail.co.uk",
"outlook.com", "live.com", "msn.com", "icloud.com", "me.com", "aol.com", "proton.me",
"protonmail.com", "gmx.com", "gmx.de", "web.de", "mail.com", "ymail.com", "comcast.net",
];
const SUFFIXES = ["com", "net", "org", "co.uk", "de", "fr", "io", "me", "edu", "gov", "ca", "com.au"];
/** Optimal string alignment distance: insertions, deletions, substitutions, adjacent swaps. */
export function distance(a: string, b: string): number {
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
for (let j = 1; j <= b.length; j++) d[0][j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
}
}
}
return d[a.length][b.length];
}
function closest(input: string, candidates: string[], max: number): string | undefined {
let best: string | undefined;
let bestD = Infinity;
let tie = false;
for (const c of candidates) {
const dist = distance(input, c);
if (dist < bestD) { best = c; bestD = dist; tie = false; }
else if (dist === bestD) tie = true;
}
return bestD > 0 && bestD <= max && !tie ? best : undefined;
}
export function suggestEmail(raw: string): string | undefined {
const at = raw.lastIndexOf("@");
if (at < 1) return undefined;
const local = raw.slice(0, at);
const domain = raw.slice(at + 1).trim().toLowerCase();
if (!domain || PROVIDERS.includes(domain)) return undefined;
// 1. Whole-domain match against popular providers (catches gmial.com, hotmial.co.uk).
const whole = closest(domain, PROVIDERS, domain.length > 8 ? 2 : 1);
if (whole) return `${local}@${whole}`;
// 2. Suffix-only fix for unknown domains (acme.con → acme.com), keeping the name part.
const dot = domain.indexOf(".");
if (dot > 0) {
const name = domain.slice(0, dot);
const suffix = domain.slice(dot + 1);
if (!SUFFIXES.includes(suffix)) {
const fixed = closest(suffix, SUFFIXES, 1);
if (fixed) return `${local}@${name}.${fixed}`;
}
}
return undefined;
}
// Wiring: suggestion on blur, applied with one click, never affecting validity.
const email = document.querySelector<HTMLInputElement>("#email")!;
const box = document.querySelector<HTMLElement>("#email-suggest")!; // referenced by aria-describedby
const live = document.querySelector<HTMLElement>("#email-suggest-live")!; // role="status"
let announced = "";
email.addEventListener("blur", () => {
if (!email.validity.valid) return (box.hidden = true); // syntax errors take priority
const s = suggestEmail(email.value);
if (!s) return (box.hidden = true);
box.replaceChildren("Did you mean ");
const btn = Object.assign(document.createElement("button"), { type: "button", textContent: s });
btn.addEventListener("click", () => {
email.value = s;
email.dispatchEvent(new Event("input", { bubbles: true }));
box.hidden = true;
email.focus();
});
box.append(btn, "?");
box.hidden = false;
if (announced !== s) { live.textContent = `Did you mean ${s}?`; announced = s; }
});
email.addEventListener("input", () => { box.hidden = true; });
The local part is preserved exactly as typed — including case and any plus-tag — because only the domain is being corrected. The suggestion is appended to the field’s description rather than injected as an error, so reportValidity() never mentions it and the form submits normally if the user ignores it.
Suggestion Engine Option Reference
| Option | Type | Default | Purpose |
|---|---|---|---|
PROVIDERS |
string[] |
~20 consumer domains | Targets for whole-domain matching |
SUFFIXES |
string[] |
common TLDs | Targets for suffix-only fixes |
| Distance metric | OSA (Damerau) | swaps cost 1 | gmial → gmail is one edit, not two |
| Max distance | number |
1 for short, 2 for long domains | Avoids suggesting gmail for gmx |
| Tie rule | boolean | no suggestion on ties | Ambiguous matches are not guessed |
| Trigger | event | blur |
No suggestions while typing |
| Announcement | status region | once per distinct suggestion | Avoids repeated interruptions |
Verification Steps
import { describe, it, expect } from "vitest";
import { suggestEmail } from "./suggest";
describe("suggestEmail", () => {
it.each([
["ada@gmial.com", "ada@gmail.com"],
["Ada+news@hotmial.co.uk", "Ada+news@hotmail.co.uk"],
["ada@yahooo.com", "ada@yahoo.com"],
["ada@acme.con", "ada@acme.com"],
])("%s → %s", (input, expected) => expect(suggestEmail(input)).toBe(expected));
it.each(["ada@gmail.com", "ada@gmx.com", "ada@example.org", "ada@mail.com"])("leaves %s alone", (input) => {
expect(suggestEmail(input)).toBeUndefined();
});
});
Edge Cases and Failure Modes
Suggesting a different real domain. mail.com is a single insertion away from gmail.com, and ymail.com is one substitution away from it — yet all three are real providers. That is why the provider list contains all of them: an exact match to any listed domain short-circuits before suggestions are considered, so ada@mail.com is never “corrected” to Gmail. Extend the list with the providers your own users actually have; your sign-up data is the best source.
Company domains near popular ones. outlok.com might be a typo or a small business. Keep the maximum distance low (1 for short names) and never block. If complaints appear, add the domain to an allow-list that suppresses suggestions.
Suggestions on every keystroke. Suggesting while the user is still typing gmai produces noise. Only suggest on blur, and hide the suggestion as soon as the user edits the field.
Server-side confirmation is still required. Suggestions reduce typos; they do not prove the address works. Send a confirmation email and treat the address as unverified until the link is followed.
Server-Side Suggestions for Other Entry Points
Not every email address reaches you through this form. Addresses arrive from checkout pages, imported contact lists, support tools and mobile apps, and the same typos appear in all of them. Moving suggestEmail into a shared module lets the server flag probable typos on those paths too — not to reject them, but to mark them for a confirmation step or a gentle “please check your email address” prompt the next time the user signs in. Keep the provider list in one place so every entry point agrees on what counts as a near miss.
Measuring Whether Suggestions Help
Typo suggestions are cheap to ship and easy to get subtly wrong, so measure them. Log, without the address itself, three events: a suggestion was shown, it was accepted, and the confirmation email for the final address was eventually opened. A healthy implementation shows a high acceptance rate and a lower bounce rate among users who accepted than among those who ignored a suggestion. A low acceptance rate on a particular rule — say, suffix fixes — usually means it suggests corrections to real domains, and that rule should be tightened or removed. Pair this with your mail provider’s bounce reports: domains that bounce often and sit close to a popular provider are candidates for the list, while frequent real domains near providers belong on the allow-list. The testing approach for the suggestion UI itself, including checking that the status announcement fires once, is covered in asserting aria-live announcements in Playwright.
Frequently Asked Questions
Should a mistyped email domain block form submission?
No. Many valid domains are one or two edits away from popular ones. Show a "did you mean" suggestion the user can accept with one click, but let the form submit if they ignore it.
How do I detect typos like gmial.com?
Compare the typed domain with a list of popular providers using an edit distance that counts adjacent swaps as one edit, and suggest the closest match when it is within one or two edits and not tied with another candidate.
When should the email suggestion appear?
On blur, after the syntax check passes. Suggesting while the user is typing produces constant noise, and suggestions for syntactically invalid addresses compete with the real error.
Is the suggestion accessible to screen reader users?
Make it a real button inside the field's description, announce it once through a polite status region, and return focus to the email field after it is applied.
Related Guides
- Identity Text Field Validation — syntax, policy and likelihood rules for identity fields.
- Email Input Validation Attributes — the native syntax check this builds on.
- Implementing Async Email Availability Checks — checking whether an address is already registered.
- Designing Accessible Error Toast Notifications — why suggestions belong inline rather than in a toast.