Two boxes, one pattern, one sample
This page sits on the same two-textarea layout as Diff Text, which is why the boxes carry generic labels. The box labeled “Original text” holds the pattern. The box labeled “Changed text” holds the sample you want to run it against. Paste the pattern bare, without the slashes a JavaScript literal would need, so \d{4} rather than the slash-wrapped form.
Dropping the slash notation was a deliberate call when this tool was built. Slash delimiters look familiar, but they break on exactly the patterns people test most often: a URL path, a Windows or POSIX file path, a date written as \d{4}/\d{2}/\d{2}. Every one of those contains an unescaped forward slash, and a parser that split on the last slash to find the flags would either mangle the pattern or force you to escape characters that need no escaping. A bare pattern plus four flag checkboxes has no such trap, and the checkboxes double as documentation of what each flag does.
One small detail worth knowing: only leading and trailing newline characters are stripped from the pattern box, never spaces. A pattern that genuinely ends in a space, like error when you are matching a log prefix, survives intact. Trimming it would quietly change what you asked to test.
Steps for a first test run
- Paste your pattern into the first box.
- Paste real sample lines into the second box, including one or two you expect not to match. A pattern that only ever sees passing input is not tested, it is merely confirmed.
- Set the flags. Leave “Find all matches” on unless you specifically want first-match behaviour.
- Click the button and read the report below.
Reading the report: matches, indexes and capture groups
The output is one plain-text report, built fresh on every click. It opens by echoing the pattern back in /pattern/flags form, so you can confirm at a glance that the flags you thought you set are the flags in play. Then comes a one-line description of each active flag, a validity status, a total match count, and one block per match.
Each block gives the matched text, the index range it occupies, every numbered capture group, and every named group if the pattern uses the (?<name>...) syntax. Groups are where most real debugging happens. A group that participated but captured nothing is printed as (did not match) rather than as an empty pair of quotes, and that distinction is usually the answer to “why did my script get undefined instead of a value”. An optional group that never fired and a group that matched an empty string look identical in most editors; here they do not.
Underneath the match list, your sample text is reprinted whole with every match bracketed as »matched«, left to right, non-overlapping. Reading that block is faster than mentally applying index arithmetic to a list of numbers.
The four flags, and which one usually explains the surprise
g decides whether you get every match or only the first. It is on by default because that is what testing against a sample almost always means, and turning it off is mainly useful when you want to reproduce how a non-global pattern behaves inside real code.
i makes the whole pattern case-insensitive. m changes what ^ and $ mean: with it on they anchor to the start and end of each line, with it off they anchor to the start and end of the entire text. Multi-line samples are where a pattern most often looks broken for no visible reason, and this checkbox is usually the fix.
s is the one people forget. By default the dot does not match a newline character, so a pattern like start.*end will never span two lines no matter how many quantifiers you add. Turn on the s checkbox and the dot covers newlines too. If you are trying to pull a multi-line block out of a config file or a stack trace, start there.
Zero-length matches and the empty marker pairs
Matches are collected with String.prototype.matchAll when the g flag is on, rather than a hand-rolled loop around exec. That is not a stylistic preference. A manual loop has to notice zero-length matches itself and nudge the cursor forward, and forgetting to do so is the classic way to hang a browser tab on a pattern like a*. matchAll handles the advance per the language spec, which is why a pattern that can match nothing still terminates here instead of spinning.
The visible consequence is that patterns able to match an empty string produce a lot of »« pairs with nothing between them. Those are genuine zero-length matches at genuine positions, counted in the total. They are not filtered out, because hiding them would make the match count and the index list disagree with what the pattern actually did.
What it will not do, and where your sample text goes
There is no token-by-token plain-English explainer here. Walking a pattern and narrating every construct is a large feature with a lot of room to be subtly wrong, and a wrong explanation of a regex is worse than none. What you get instead is an unambiguous test report: valid or not, which flags, how many matches, where, and what each group captured.
The sample text you paste is rarely synthetic. It is a real log line with a customer address in it, a copied support ticket, a chunk of an API response, a key format you would rather not publish. Nothing here is transmitted: the browser’s own RegExp object is a complete regex engine, no library is loaded for it, and the report is assembled locally the moment you click. Once the page has loaded you can go offline and it still works exactly the same. If you want to double-check the structure of that sample first, JSON Formatter and Case Converter run the same way.
![Screenshot of the Regex Tester & Builder tool with the sample input “\b[\w.+-]+@[\w-]+\.[\w.]+\b”, Find all matches (g flag), off finds only the first match set to on, Case-insensitive (i flag) set to off](/shots/regex-builder-ui.webp)
![Screenshot of the Regex Tester & Builder result screen showing the generated output “=== REGEX TEST RESULTS === Pattern: /\b[\w.+-]+@…”](/shots/regex-builder-result.webp)