Loading
Create a regex testing tool with live pattern matching, capture group highlighting, flag toggles, match visualization, and a cheat sheet sidebar.
Regular expressions are one of the most powerful tools in a developer's toolkit, but they are notoriously difficult to write and debug. A good regex tester provides instant visual feedback: you type a pattern, and the matches light up in the test string immediately. You see capture groups, match indices, and flags all at once.
In this tutorial, you will build a regex tester with live pattern matching that updates on every keystroke, capture group extraction with color-coded highlighting, flag toggles for global, case-insensitive, multiline, and other regex flags, a match details panel showing index positions and group values, and a cheat sheet sidebar for quick reference. The entire tool runs client-side — no server needed.
This project is pure HTML, CSS, and JavaScript. No build step required.
Create a single index.html file. We will build everything in this one file for simplicity, then extract modules in later steps.
Create index.html:
Create app.js starting with the core matching logic:
The safety counter prevents infinite loops when a pattern matches empty strings (like /a*/g). Without it, exec would match at the same index forever.
Add the highlighting logic to app.js:
Capture groups get distinct colors so you can visually distinguish which part of the pattern matched which part of the text. This is critical for debugging complex patterns with multiple groups.
Continue in app.js:
Continue in app.js:
Continue in app.js:
Complete app.js with the main update loop:
To serve the file, use any static server. On macOS, Linux, or Windows with Node.js installed:
Open http://localhost:3006. The default pattern (\w+)@(\w+)\.(\w+) matches email addresses in the test string. Each capture group (username, domain, TLD) shows in a different color.
Test these patterns to explore the tool: \d{3}-\d{4} for phone number fragments, ^.+$ with the multiline flag to match each line, (?<=@)\w+ for lookbehind (extracts domain names), \b\w{5}\b for exactly 5-letter words. Toggle the i flag and notice how patterns match differently. Type an invalid pattern like [ and see the error message appear immediately.
The tool updates on every keystroke with zero delay because regex matching is fast on modern engines. Even complex patterns against kilobytes of text execute in under a millisecond. The cheat sheet sidebar provides quick reference without leaving the tool, reducing the context-switching cost of regex development.
mkdir regex-tester && cd regex-tester<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Regex Tester</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background: #0a0a0f;
color: #f0f0f0;
display: grid;
grid-template-columns: 1fr 280px;
min-height: 100vh;
}
.main {
padding: 24px;
overflow-y: auto;
}
h1 {
font-size: 22px;
margin-bottom: 20px;
}
.pattern-row {
display: flex;
gap: 8px;
margin-bottom: 16px;
align-items: center;
}
.pattern-input {
flex: 1;
padding: 10px 14px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: #f0f0f0;
font-family: "JetBrains Mono", monospace;
font-size: 15px;
}
.pattern-input:focus {
outline: none;
border-color: #10b981;
}
.pattern-input.error {
border-color: #ef4444;
}
.slash {
color: #6b6b75;
font-size: 18px;
font-family: "JetBrains Mono", monospace;
}
.flags-input {
width: 80px;
padding: 10px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: #10b981;
font-family: "JetBrains Mono", monospace;
font-size: 15px;
text-align: center;
}
.error-msg {
color: #ef4444;
font-size: 13px;
margin-bottom: 12px;
min-height: 18px;
}
.flags-toggles {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.flag-btn {
padding: 4px 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
background: transparent;
color: #a0a0a8;
font-size: 12px;
cursor: pointer;
font-family: "JetBrains Mono", monospace;
}
.flag-btn.active {
background: rgba(16, 185, 129, 0.1);
border-color: #10b981;
color: #10b981;
}
.test-string {
width: 100%;
min-height: 200px;
padding: 14px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: #f0f0f0;
font-family: "JetBrains Mono", monospace;
font-size: 14px;
line-height: 1.8;
resize: vertical;
margin-bottom: 16px;
}
.test-string:focus {
outline: none;
border-color: #10b981;
}
.results-label {
font-size: 13px;
color: #6b6b75;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.highlighted {
padding: 14px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
font-family: "JetBrains Mono", monospace;
font-size: 14px;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
min-height: 60px;
margin-bottom: 16px;
}
.match-highlight {
background: rgba(16, 185, 129, 0.25);
border-radius: 2px;
padding: 1px 0;
}
.group-1 {
background: rgba(147, 197, 253, 0.25);
}
.group-2 {
background: rgba(196, 181, 253, 0.25);
}
.group-3 {
background: rgba(253, 164, 175, 0.25);
}
.group-4 {
background: rgba(253, 186, 116, 0.25);
}
.matches-panel {
margin-bottom: 16px;
}
.match-item {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 6px;
font-size: 13px;
}
.match-item .match-text {
color: #10b981;
font-family: "JetBrains Mono", monospace;
}
.match-item .match-index {
color: #6b6b75;
font-size: 11px;
}
.match-item .group-label {
color: #a0a0a8;
font-size: 11px;
}
.match-item .group-value {
color: #93c5fd;
font-family: "JetBrains Mono", monospace;
}
.sidebar {
background: rgba(255, 255, 255, 0.02);
border-left: 1px solid rgba(255, 255, 255, 0.08);
padding: 24px;
overflow-y: auto;
}
.sidebar h2 {
font-size: 16px;
margin-bottom: 16px;
}
.cheat-section {
margin-bottom: 20px;
}
.cheat-section h3 {
font-size: 12px;
color: #6b6b75;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.cheat-item {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 13px;
}
.cheat-item code {
color: #10b981;
font-family: "JetBrains Mono", monospace;
}
.cheat-item span {
color: #a0a0a8;
}
.stats {
font-size: 13px;
color: #a0a0a8;
margin-bottom: 16px;
}
@media (max-width: 768px) {
body {
grid-template-columns: 1fr;
}
.sidebar {
border-left: none;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
}
</style>
</head>
<body>
<div class="main">
<h1>Regex Tester</h1>
<div class="pattern-row">
<span class="slash">/</span>
<input
type="text"
class="pattern-input"
id="patternInput"
value="(\w+)@(\w+)\.(\w+)"
placeholder="Enter regex pattern"
spellcheck="false"
/>
<span class="slash">/</span>
<input
type="text"
class="flags-input"
id="flagsInput"
value="g"
placeholder="flags"
spellcheck="false"
/>
</div>
<div class="error-msg" id="errorMsg"></div>
<div class="flags-toggles" id="flagToggles"></div>
<textarea
class="test-string"
id="testString"
spellcheck="false"
placeholder="Enter test string..."
>
Contact us at hello@example.com or support@company.org for help.
You can also reach admin@test.net directly.</textarea
>
<div class="stats" id="stats"></div>
<div class="results-label">Highlighted Matches</div>
<div class="highlighted" id="highlighted"></div>
<div class="results-label">Match Details</div>
<div class="matches-panel" id="matchesPanel"></div>
</div>
<aside class="sidebar">
<h2>Cheat Sheet</h2>
<div id="cheatSheet"></div>
</aside>
<script src="app.js"></script>
</body>
</html>const patternInput = document.getElementById("patternInput");
const flagsInput = document.getElementById("flagsInput");
const testString = document.getElementById("testString");
const errorMsg = document.getElementById("errorMsg");
const highlighted = document.getElementById("highlighted");
const matchesPanel = document.getElementById("matchesPanel");
const statsEl = document.getElementById("stats");
const flagToggles = document.getElementById("flagToggles");
function tryCreateRegex(pattern, flags) {
try {
const regex = new RegExp(pattern, flags);
return { regex, error: null };
} catch (e) {
return { regex: null, error: e.message };
}
}
function escapeHtml(text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function getMatches(regex, text) {
const matches = [];
if (!regex.global) {
const match = regex.exec(text);
if (match) matches.push(match);
return matches;
}
let match;
let safety = 0;
regex.lastIndex = 0;
while ((match = regex.exec(text)) !== null && safety < 10000) {
matches.push(match);
if (match[0].length === 0) regex.lastIndex++;
safety++;
}
return matches;
}const GROUP_CLASSES = ["", "group-1", "group-2", "group-3", "group-4"];
function buildHighlightedHtml(text, matches) {
if (matches.length === 0) return escapeHtml(text);
const parts = [];
let lastIndex = 0;
for (const match of matches) {
const matchStart = match.index;
const matchEnd = matchStart + match[0].length;
if (matchStart > lastIndex) {
parts.push(escapeHtml(text.slice(lastIndex, matchStart)));
}
if (match.length > 1) {
// Highlight capture groups within the match
let innerHtml = "";
let innerIndex = 0;
const fullMatch = match[0];
for (let g = 1; g < match.length; g++) {
if (match[g] === undefined) continue;
const groupStart = fullMatch.indexOf(match[g], innerIndex);
if (groupStart === -1) continue;
if (groupStart > innerIndex) {
innerHtml += `<span class="match-highlight">${escapeHtml(fullMatch.slice(innerIndex, groupStart))}</span>`;
}
const groupClass = GROUP_CLASSES[g] || GROUP_CLASSES[GROUP_CLASSES.length - 1];
innerHtml += `<span class="match-highlight ${groupClass}">${escapeHtml(match[g])}</span>`;
innerIndex = groupStart + match[g].length;
}
if (innerIndex < fullMatch.length) {
innerHtml += `<span class="match-highlight">${escapeHtml(fullMatch.slice(innerIndex))}</span>`;
}
parts.push(innerHtml);
} else {
parts.push(`<span class="match-highlight">${escapeHtml(match[0])}</span>`);
}
lastIndex = matchEnd;
}
if (lastIndex < text.length) {
parts.push(escapeHtml(text.slice(lastIndex)));
}
return parts.join("");
}function renderMatchDetails(matches) {
if (matches.length === 0) {
matchesPanel.innerHTML = '<div class="match-item" style="color:#6b6b75;">No matches</div>';
return;
}
matchesPanel.innerHTML = matches
.map((match, i) => {
let groupsHtml = "";
for (let g = 1; g < match.length; g++) {
if (match[g] !== undefined) {
groupsHtml += `<div><span class="group-label">Group ${g}:</span> <span class="group-value">${escapeHtml(match[g])}</span></div>`;
}
}
return `<div class="match-item">
<div>
<span class="match-text">${escapeHtml(match[0])}</span>
<span class="match-index">index ${match.index}–${match.index + match[0].length}</span>
</div>
${groupsHtml}
</div>`;
})
.join("");
}
function updateStats(matches, text) {
const charCount = matches.reduce((sum, m) => sum + m[0].length, 0);
statsEl.textContent = `${matches.length} match${matches.length === 1 ? "" : "es"} found, ${charCount} characters matched`;
}const FLAGS = [
{ flag: "g", label: "global", desc: "Find all matches" },
{ flag: "i", label: "insensitive", desc: "Case-insensitive" },
{ flag: "m", label: "multiline", desc: "^ and $ match line boundaries" },
{ flag: "s", label: "dotAll", desc: ". matches newlines" },
{ flag: "u", label: "unicode", desc: "Unicode support" },
];
function renderFlagToggles() {
flagToggles.innerHTML = FLAGS.map(
(f) =>
`<button class="flag-btn ${flagsInput.value.includes(f.flag) ? "active" : ""}" data-flag="${f.flag}" title="${f.desc}">${f.flag} ${f.label}</button>`
).join("");
flagToggles.querySelectorAll(".flag-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const flag = btn.dataset.flag;
const current = flagsInput.value;
flagsInput.value = current.includes(flag) ? current.replace(flag, "") : current + flag;
update();
});
});
}const CHEAT_SHEET = [
{
title: "Character Classes",
items: [
{ pattern: ".", desc: "Any character" },
{ pattern: "\\w", desc: "Word character" },
{ pattern: "\\d", desc: "Digit" },
{ pattern: "\\s", desc: "Whitespace" },
{ pattern: "\\W", desc: "Not word char" },
{ pattern: "\\D", desc: "Not digit" },
{ pattern: "[abc]", desc: "a, b, or c" },
{ pattern: "[^abc]", desc: "Not a, b, or c" },
{ pattern: "[a-z]", desc: "Range a to z" },
],
},
{
title: "Quantifiers",
items: [
{ pattern: "*", desc: "0 or more" },
{ pattern: "+", desc: "1 or more" },
{ pattern: "?", desc: "0 or 1" },
{ pattern: "{3}", desc: "Exactly 3" },
{ pattern: "{3,}", desc: "3 or more" },
{ pattern: "{3,5}", desc: "3 to 5" },
{ pattern: "*?", desc: "Lazy 0+" },
],
},
{
title: "Anchors",
items: [
{ pattern: "^", desc: "Start of string" },
{ pattern: "$", desc: "End of string" },
{ pattern: "\\b", desc: "Word boundary" },
],
},
{
title: "Groups",
items: [
{ pattern: "(abc)", desc: "Capture group" },
{ pattern: "(?:abc)", desc: "Non-capturing" },
{ pattern: "a|b", desc: "Alternation" },
{ pattern: "\\1", desc: "Backreference" },
{ pattern: "(?=abc)", desc: "Lookahead" },
{ pattern: "(?!abc)", desc: "Neg. lookahead" },
],
},
];
function renderCheatSheet() {
document.getElementById("cheatSheet").innerHTML = CHEAT_SHEET.map(
(section) => `
<div class="cheat-section">
<h3>${section.title}</h3>
${section.items.map((item) => `<div class="cheat-item"><code>${escapeHtml(item.pattern)}</code><span>${item.desc}</span></div>`).join("")}
</div>`
).join("");
}
renderCheatSheet();function update() {
const pattern = patternInput.value;
const flags = flagsInput.value;
if (!pattern) {
errorMsg.textContent = "";
highlighted.innerHTML = escapeHtml(testString.value);
matchesPanel.innerHTML = "";
statsEl.textContent = "";
renderFlagToggles();
return;
}
const { regex, error } = tryCreateRegex(pattern, flags);
if (error) {
errorMsg.textContent = error;
patternInput.classList.add("error");
highlighted.innerHTML = escapeHtml(testString.value);
matchesPanel.innerHTML = "";
statsEl.textContent = "";
renderFlagToggles();
return;
}
errorMsg.textContent = "";
patternInput.classList.remove("error");
const text = testString.value;
const matches = getMatches(regex, text);
highlighted.innerHTML = buildHighlightedHtml(text, matches);
renderMatchDetails(matches);
updateStats(matches, text);
renderFlagToggles();
}
patternInput.addEventListener("input", update);
flagsInput.addEventListener("input", update);
testString.addEventListener("input", update);
// Initial render
update();npx serve . -p 3006