What You'll Build

A regex tester that accepts a pattern and a test string, then highlights every match in the text. It shows capture groups, lets you toggle regex flags (g, i, m, s), and displays match statistics.

  • Pattern input and test string input
  • Live highlighting of every match
  • Flag toggles: g (global), i (case-insensitive), m (multiline), s (dotall)
  • Capture group display
  • Match count and execution time
  • Copy matches button
▶ Live Preview — this is what you'll build

What You'll Learn

Why this is advanced: Regex has its own mini-language, and building a tool that highlights matches safely (without breaking HTML) requires careful string handling. The result is one of the most-used tools in every developer's bookmarks.

1

Build the HTML Structure

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Regex Tester</title>
</head>
<body>
    <div class="card">
        <h1>Regex Tester</h1>
        <p class="sub">Test regular expressions with live highlighting</p>

        <div class="field">
            <label>Regular Expression</label>
            <div class="regex-input-wrap">
                <span class="slash">/</span>
                <input type="text" id="pattern" value="\b\w+@\w+\.\w+\b" spellcheck="false">
                <span class="slash">/</span>
                <span class="flags-display" id="flagsDisplay">g</span>
            </div>
        </div>

        <div class="flag-row">
            <button class="flag-btn active" data-flag="g">g global</button>
            <button class="flag-btn" data-flag="i">i ignore case</button>
            <button class="flag-btn" data-flag="m">m multiline</button>
            <button class="flag-btn" data-flag="s">s dotall</button>
        </div>

        <div class="field">
            <label>Test String</label>
            <textarea id="testString">Contact us at hello@makuistudio.com or support@example.org.</textarea>
        </div>

        <div class="error" id="errorMsg"></div>

        <div class="stats">
            <span>Matches: <strong id="matchCount">0</strong></span>
            <span>Time: <strong id="matchTime">0 ms</strong></span>
        </div>

        <div class="output" id="output"></div>

        <div class="groups">
            <h3>Capture Groups</h3>
            <div id="groupsList"></div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>
2

Style It with CSS

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f1f5f9;
    padding: 20px;
    color: #0f172a;
}

.card {
    max-width: 580px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}

h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }

.field { margin-bottom: 10px; }
label {
    display: block;
    font-size: 0.75rem;
    font-weight: 600;
    color: #334155;
    margin-bottom: 4px;
}

/* Regex pattern input — styled to look like /pattern/flags */
.regex-input-wrap {
    display: flex;
    align-items: center;
    background: #f8fafc;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    padding: 0 4px 0 12px;
    transition: 0.2s;
}
.regex-input-wrap:focus-within {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
.slash {
    color: #3b82f6;
    font-family: "Courier New", monospace;
    font-weight: 700;
    font-size: 1rem;
}
.regex-input-wrap input {
    flex: 1;
    border: none;
    background: transparent;
    padding: 10px 6px;
    font-size: 0.9rem;
    font-family: "Courier New", monospace;
    outline: none;
}
.flags-display {
    color: #64748b;
    font-family: "Courier New", monospace;
    font-weight: 700;
    padding: 0 12px 0 4px;
}

/* Test string textarea */
textarea {
    width: 100%;
    min-height: 100px;
    padding: 10px 12px;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    font-size: 0.85rem;
    font-family: "Courier New", monospace;
    outline: none;
    resize: vertical;
    line-height: 1.6;
}
textarea:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

/* Flag toggles */
.flag-row {
    display: flex;
    gap: 5px;
    margin-bottom: 12px;
    flex-wrap: wrap;
}
.flag-btn {
    background: #f8fafc;
    border: 2px solid #e2e8f0;
    color: #64748b;
    padding: 5px 10px;
    border-radius: 6px;
    font-weight: 700;
    font-size: 0.72rem;
    cursor: pointer;
    transition: 0.2s;
    font-family: "Courier New", monospace;
}
.flag-btn:hover { border-color: #cbd5e1; }
.flag-btn.active {
    background: #eff6ff;
    border-color: #3b82f6;
    color: #1d4ed8;
}

/* Error box */
.error {
    background: #fef2f2;
    border: 1px solid #fca5a5;
    color: #dc2626;
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 0.78rem;
    font-family: "Courier New", monospace;
    margin: 8px 0;
    display: none;
}
.error.show { display: block; }

/* Stats */
.stats {
    display: flex;
    gap: 12px;
    font-size: 0.72rem;
    color: #64748b;
    margin: 8px 0;
    flex-wrap: wrap;
}
.stats strong { color: #334155; font-family: "Courier New", monospace; }

/* Output box — highlights matches with mark */
.output {
    background: #fff;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    padding: 12px 14px;
    font-family: "Courier New", monospace;
    font-size: 0.85rem;
    line-height: 1.7;
    white-space: pre-wrap;
    word-break: break-word;
    min-height: 100px;
    max-height: 220px;
    overflow-y: auto;
}
.output mark {
    background: #3b82f6;
    color: #fff;
    padding: 1px 3px;
    border-radius: 3px;
}
.output mark.alt {
    background: #8b5cf6;
}

/* Capture groups */
.groups { margin-top: 12px; }
.groups h3 {
    font-size: 0.78rem;
    font-weight: 700;
    color: #0f172a;
    margin-bottom: 6px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.group-item {
    background: #f8fafc;
    border-radius: 6px;
    padding: 6px 10px;
    font-family: "Courier New", monospace;
    font-size: 0.75rem;
    color: #334155;
    margin-bottom: 4px;
}
.group-item .lbl {
    color: #64748b;
    font-weight: 700;
    margin-right: 6px;
}
.group-item .val {
    color: #1d4ed8;
}
.empty {
    color: #94a3b8;
    font-style: italic;
    font-size: 0.85rem;
}
3

Write the JavaScript — Regex Engine

JavaScript — Part 1: Setup + Run
// ============================================================
// STATE + DOM
// ============================================================

var currentFlags = "g";

var patternEl = document.getElementById("pattern");
var testEl = document.getElementById("testString");
var outputEl = document.getElementById("output");
var errorEl = document.getElementById("errorMsg");
var matchCountEl = document.getElementById("matchCount");
var matchTimeEl = document.getElementById("matchTime");
var groupsEl = document.getElementById("groupsList");
var flagsDisplayEl = document.getElementById("flagsDisplay");

// ============================================================
// ESCAPE HTML — safety before injecting into innerHTML
// ============================================================
// If the test string contains < or >, we must escape them
// before wrapping matched parts in  tags. Otherwise
// the browser would interpret user input as HTML.

function escapeHtml(str) {
    return String(str)
        .replace(/&/g, "&")
        .replace(//g, ">")
        .replace(/"/g, """);
}

// ============================================================
// RUN — the main function
// ============================================================
// 1. Read pattern and flags
// 2. Build a RegExp object
// 3. Find matches (with or without global flag)
// 4. Time it
// 5. Render highlights and capture groups

function run() {
    var pat = patternEl.value;
    var text = testEl.value;

    errorEl.classList.remove("show");
    flagsDisplayEl.textContent = currentFlags || "(none)";

    // Empty pattern → show plain text
    if (!pat) {
        outputEl.textContent = text;
        matchCountEl.textContent = "0";
        matchTimeEl.textContent = "0 ms";
        groupsEl.innerHTML = '<div class="empty">No matches yet</div>';
        return;
    }

    // Try to compile the pattern. Bad patterns throw.
    var regex;
    try {
        regex = new RegExp(pat, currentFlags);
    } catch (e) {
        errorEl.textContent = "❌ " + e.message;
        errorEl.classList.add("show");
        outputEl.textContent = text;
        matchCountEl.textContent = "0";
        matchTimeEl.textContent = "0 ms";
        groupsEl.innerHTML = '<div class="empty">Fix the pattern to see matches</div>';
        return;
    }

    // ---- Find matches ----
    var t0 = performance.now();
    var matches = [];

    if (currentFlags.indexOf("g") !== -1) {
        // Global mode — iterate with exec()
        var iter = new RegExp(pat, currentFlags);
        var m;
        var safety = 0;
        while ((m = iter.exec(text)) !== null && safety < 5000) {
            matches.push({
                index: m.index,
                length: m[0].length,
                full: m[0],
                groups: m.slice(1)
            });
            // Handle zero-width matches to avoid infinite loops
            if (m[0] === "") iter.lastIndex++;
            safety++;
        }
    } else {
        // Single mode — one match max
        var single = regex.exec(text);
        if (single) {
            matches.push({
                index: single.index,
                length: single[0].length,
                full: single[0],
                groups: single.slice(1)
            });
        }
    }

    var t1 = performance.now();

    // ---- Update display ----
    matchCountEl.textContent = matches.length;
    matchTimeEl.textContent = (t1 - t0).toFixed(2) + " ms";

    renderHighlighted(text, matches);
    renderGroups(matches);
}

Why new RegExp(pat, flags) instead of literal syntax? Regex literals like /\w+/g are known at code-time. But our pattern comes from user input at runtime, so we must use the constructor to build a regex from a string.

Zero-width match safety: Some patterns match an empty string (like \b alone). Without the if (m[0] === "") iter.lastIndex++ guard, the loop would never end. This is a classic regex pitfall.

4

Write the JavaScript — Rendering

JavaScript — Part 2: Render
// ============================================================
// HIGHLIGHT MATCHES
// ============================================================
// Build the HTML by walking through the text once, inserting
// <mark> tags around each match. Alternate colors help
// distinguish adjacent matches.

function renderHighlighted(text, matches) {
    if (matches.length === 0) {
        outputEl.textContent = text || "(empty string)";
        return;
    }

    var html = "";
    var cursor = 0;

    for (var i = 0; i < matches.length; i++) {
        var m = matches[i];

        // Text before this match (escaped)
        html += escapeHtml(text.slice(cursor, m.index));

        // The match itself, wrapped in <mark>
        var cls = i % 2 === 1 ? ' class="alt"' : '';
        html += '<mark' + cls + '>' +
                escapeHtml(text.slice(m.index, m.index + m.length)) +
                '</mark>';

        cursor = m.index + m.length;
    }

    // Remaining text after the last match
    html += escapeHtml(text.slice(cursor));
    outputEl.innerHTML = html;
}

// ============================================================
// RENDER CAPTURE GROUPS
// ============================================================
// A regex like (\d+)-(\d+) has two capture groups.
// matches[i].groups = [group1, group2, ...]

function renderGroups(matches) {
    if (matches.length === 0) {
        groupsEl.innerHTML = '<div class="empty">No matches yet</div>';
        return;
    }

    var shown = matches.slice(0, 15);
    groupsEl.innerHTML = shown.map(function(m, i) {
        var groupHtml = m.groups.length > 0
            ? m.groups.map(function(g, gi) {
                return '<span class="lbl">$' + (gi + 1) + ':</span>' +
                       '<span class="val">' + escapeHtml(String(g)) + '</span>';
            }).join(" ")
            : '<span class="lbl">(no capture groups)</span>';

        return '<div class="group-item">' +
               '<span class="lbl">Match ' + (i + 1) + ':</span>' +
               groupHtml +
               '</div>';
    }).join("");

    if (matches.length > 15) {
        groupsEl.innerHTML += '<div class="group-item"><span class="lbl">…and ' + (matches.length - 15) + ' more</span></div>';
    }
}
5

Write the JavaScript — Flags & Events

JavaScript — Part 3: Events
// ============================================================
// FLAG TOGGLES
// ============================================================
// Each flag button toggles its flag on/off. We keep the
// flags in a single string, and re-order them canonically
// so "gi" and "ig" are treated as the same.

document.querySelectorAll(".flag-btn").forEach(function(btn) {
    btn.addEventListener("click", function() {
        var f = btn.dataset.flag;

        if (currentFlags.indexOf(f) !== -1) {
            currentFlags = currentFlags.replace(f, "");
            btn.classList.remove("active");
        } else {
            currentFlags += f;
            btn.classList.add("active");
        }

        // Normalize order
        currentFlags = ["g", "i", "m", "s"].filter(function(x) {
            return currentFlags.indexOf(x) !== -1;
        }).join("");

        run();
    });
});

// ============================================================
// LIVE UPDATES
// ============================================================

patternEl.addEventListener("input", run);
testEl.addEventListener("input", run);

// Boot
run();

Save and open index.html. The default email pattern highlights two emails. Try different patterns: \d+ for numbers, [A-Z]\w+ for capitalised words, or (\w+)@(\w+) for capture groups.

6

Understand the Big Ideas

1. The RegExp constructor vs literal

/\d+/g is a literal regex — the pattern is fixed at code-time. new RegExp("\\d+", "g") is a runtime regex — the pattern is built from a string. When the pattern is user input, only the second option works.

Note the double backslash: in a string, \d would be interpreted as an escape sequence. \\d gives the regex engine the actual \d it needs.

2. exec() and lastIndex — how global matching actually works

When a regex has the g flag, calling .exec() repeatedly returns each match in sequence. The regex object remembers where it stopped via .lastIndex. When there are no more matches, .exec() returns null.

JavaScript
var re = /\d+/g;
var text = "a1 b22 c333";

re.exec(text);  // ["1"]  lastIndex=2
re.exec(text);  // ["22"] lastIndex=6
re.exec(text);  // ["333"] lastIndex=12
re.exec(text);  // null   lastIndex=0

Without the g flag, .exec() always returns the first match and doesn't update .lastIndex. That's why the code branches based on whether g is set.

3. Capture groups

Parentheses in a regex create capture groups. For the pattern (\w+)@(\w+), matching "hello@example" produces:

We store m.slice(1) as groups — an array of everything except the full match. That's what the capture groups panel shows.

4. XSS-safe highlighting

The most subtle piece of this project is escapeHtml(). If a user types <script>alert(1)</script> into the test string, we must NOT let the browser execute it. Escaping turns < into &lt; so it displays as text instead of being parsed as a tag.

Rule to remember: Whenever you build HTML by concatenating strings, escape any user-controlled values first. This applies to every project — Todo lists, comments, chat apps.

7

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

Compare your solution: We built the production version with more patterns, replace mode, and copy features — see the live Regex Tester.

Full Source Code

Common Mistakes & Fixes

❌ "Regex does nothing on 'g' flag off"

Without g, .exec() returns only the first match. If you expected all matches, either add the flag or use String.prototype.match() — but that behaves differently with/without g.

❌ "Infinite loop with empty matches"

Zero-width matches (like \b or a* matching empty) cause .exec() to return forever. Always increment lastIndex when m[0] === "".

❌ "Highlighting breaks on special characters"

User input with < or & gets interpreted as HTML. Always escape before wrapping in <mark> tags. The escapeHtml() function does this.

❌ "Colons or slashes in the pattern confuse the input"

Users might type /pattern/g including the slashes. Our input only wants the pattern part. Consider stripping leading/trailing slashes automatically.

❌ "The error message is scary"

Browser regex errors are technical ("Invalid regular expression: /(/..."). Consider adding a helper that translates common errors into friendly messages.

What to Build Next