A step-by-step guide to building a real-time regular expression tester — no frameworks, no libraries, no build tools.
Regular expressions are one of the most powerful yet misunderstood tools in a developer's arsenal. They're used for form validation, text parsing, data extraction, and pattern matching across virtually every programming language.
But regex is hard to get right. That's why building a regex tester — a tool that lets you type a pattern, test it against text, and see matches highlighted in real time — is both a practical exercise and a deep dive into how regular expressions actually work.
This tutorial assumes basic familiarity with regular expressions. If you know what \w+ means but aren't sure about lookahead assertions, you're ready.
A regex tester needs three main components: a pattern input, a flags selector, and a text area to test against. Here's the skeleton:
HTML<div class="regex-tester">
<div class="input-row">
<input type="text" id="pattern" placeholder="Enter regex pattern..." />
<select id="flags">
<option value="g">g — Global</option>
<option value="gi">gi — Global + Ignore Case</option>
<option value="m">m — Multi-line</option>
<option value="gs">gs — Global + Dotall</option>
</select>
<span id="status"></span>
</div>
<textarea id="testText" placeholder="Enter text to test against..."></textarea>
<div id="results">
<h3>Matches</h3>
<div id="highlights"></div>
<div id="captures"></div>
</div>
</div>
Key design decisions:
pattern input is a single-line text field — regex patterns rarely need multi-line inputflags selector limits choices to the most common combinations, preventing typosstatus span shows validation errors (invalid regex) in real timehighlights div shows the text with matches visually highlightedcaptures div shows captured groups for each matchThe core logic is deceptively simple: take the pattern, compile it into a RegExp object, and test it against the input text. The challenge is handling errors gracefully and updating the UI in real time.
JavaScriptconst patternInput = document.getElementById('pattern');
const flagsSelect = document.getElementById('flags');
const testTextInput = document.getElementById('testText');
const statusSpan = document.getElementById('status');
const highlightsDiv = document.getElementById('highlights');
const capturesDiv = document.getElementById('captures');
function testRegex() {
const pattern = patternInput.value;
const flags = flagsSelect.value;
// Clear previous results
statusSpan.textContent = '';
statusSpan.style.color = '';
highlightsDiv.innerHTML = '';
capturesDiv.innerHTML = '';
if (!pattern) {
highlightsDiv.textContent = testTextInput.value;
return;
}
let regex;
try {
regex = new RegExp(pattern, flags);
} catch (e) {
statusSpan.textContent = 'Invalid regex: ' + e.message;
statusSpan.style.color = '#ef4444';
return;
}
const text = testTextInput.value;
const matches = [...text.matchAll(regex)];
if (matches.length === 0) {
highlightsDiv.textContent = text;
statusSpan.textContent = 'No matches found';
statusSpan.style.color = '#f59e0b';
return;
}
// Display matches
statusSpan.textContent = matches.length + ' match' + (matches.length !== 1 ? 'es' : '') + ' found';
statusSpan.style.color = '#22c55e';
renderHighlights(text, matches);
renderCaptures(matches);
}
Key insight: String.prototype.matchAll() returns an iterator of all matches with capture groups. Spreading it into an array ([...text.matchAll(regex)]) gives us a concrete array we can iterate over. This is available in modern JavaScript (ES2020) and is far superior to the older RegExp.exec() loop.
Visual feedback is what makes a regex tester useful. We need to highlight matched portions of the text while escaping HTML entities to prevent injection.
JavaScriptfunction escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function renderHighlights(text, matches) {
let html = '';
let lastIndex = 0;
matches.forEach((match, index) => {
// Text before this match
html += escapeHtml(text.slice(lastIndex, match.index));
// The matched text, highlighted
html += '<span class="match">' + escapeHtml(match[0]) + '</span>';
lastIndex = match.index + match[0].length;
});
// Remaining text after last match
html += escapeHtml(text.slice(lastIndex));
highlightsDiv.innerHTML = html;
}
This function walks through the text, inserting highlight spans at each match position. The result is a visual overlay that shows exactly where the regex matched — critical for debugging complex patterns.
The escapeHtml() function is essential. Without it, if the test text contains HTML characters like < or &, they would be interpreted as HTML tags, potentially breaking the layout or creating security issues.
The real power of regex comes from capture groups — numbered parenthetical expressions that extract specific parts of a match. A regex tester should display these clearly.
JavaScriptfunction renderCaptures(matches) {
const hasGroups = matches.some(m => m.length > 1);
if (!hasGroups) return;
let html = '<h3>Capture Groups</h3><table><thead>';
html += '<tr><th>#</th><th>Group</th><th>Value</th><th>Position</th></tr></thead><tbody>';
matches.forEach((match, matchIdx) => {
for (let i = 1; i < match.length; i++) {
html += '<tr>';
html += '<td>' + matchIdx + ':' + i + '</td>';
html += '<td>Group ' + i + '</td>';
html += '<td class="capture-group">' + escapeHtml(match[i] || '(empty)') + '</td>';
html += '<td>' + (match.index + (i < match.length ? match[i].length : 0)) + '</td>';
html += '</tr>';
}
});
html += '</tbody></table>';
capturesDiv.innerHTML = html;
}
Each match array has:
match[0] — the full matchmatch[1], match[2], etc. — captured groupsmatch.index — the position in the original stringmatch.input — the original textFor example, the pattern (\d{4})-(\d{2})-(\d{2}) against 2026-10-02 produces:
Match[
"2026-10-02", // full match
"2026", // group 1: year
"10", // group 2: month
"02", // group 3: day
index: 0,
input: "2026-10-02",
groups: undefined
]
JavaScript regex supports several flags that dramatically change matching behavior:
JavaScript// 'g' — Global: find all matches, not just the first
'hello world hello'.match(/hello/g)
// → ["hello", "hello"]
// 'i' — Ignore case: case-insensitive matching
'Hello'.match(/hello/i)
// → ["Hello"]
// 'm' — Multi-line: ^ and $ match line boundaries
'line1\nline2'.match(/^line/gm)
// → ["line", "line"]
// 's' — Dotall: . matches newlines too (ES2018+)
'a.b'.match(/a.b/s)
// → ["a.b"]
// 'u' — Unicode: proper handling of Unicode code points
'\uD83D\uDE00'.match(/\p{Emoji}/gu)
// → ["😀"]
The g flag changes how RegExp.exec() and matchAll() behave — without it, they only return the first match. With g, they return all matches. This is the most commonly used flag in regex testers.
Edge cases to handle:
SyntaxError and display the error message(a*)* can cause catastrophic backtracking; set a timeout to aborta* match at every position; skip positions where the match length is 0Here's everything together — copy this into an HTML file and open it in your browser:
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Regex Tester</title>
<style>
/* Paste the CSS from the header section */
</style>
</head>
<body>
<div class="regex-tester">
<!-- Paste the HTML from Step 1 -->
</div>
<script>
// Paste the JavaScript from Steps 2-5
patternInput.addEventListener('input', testRegex);
flagsSelect.addEventListener('change', testRegex);
testTextInput.addEventListener('input', testRegex);
</script>
</body>
</html>
The full, working implementation is available as a single HTML file — no build step required. It includes:
The fully working Regex Tester is live and free to use:
Open Regex Tester → View This TutorialAll tools on k1r4.space are free, open-source, and require no signup, tracking, or ads. They're built with vanilla HTML, CSS, and JavaScript — no frameworks, no build tools.
Building a regex tester taught us several important concepts: