Building a Visual Regex Generator: From Zero to Match#

Regular expressions are powerful but notoriously hard to remember. Even experienced developers need to look up syntax for email validation or IP matching. I built a Regex Generator to make this easier. Here’s how it works.

The Problem with Regex#

  • Cryptic syntax: \d, \w, \s are manageable, but (?=...), (?:...), (?!) are confusing
  • Hard to debug: One wrong character and nothing matches — no clear error message
  • No visualization: A long regex string is just a wall of symbols

The generator aims to solve these with clickable buttons and instant visual feedback.

Core Features#

1. Visual Builder#

Breaking regex syntax into categorized buttons:

// Character classes
const CHAR_BUTTONS = [
  { label: '.', value: '.', desc: 'Any character' },
  { label: '\\d', value: '\\d', desc: 'Digit [0-9]' },
  { label: '\\w', value: '\\w', desc: 'Word char [a-zA-Z0-9_]' },
  { label: '\\s', value: '\\s', desc: 'Whitespace' },
  { label: '\\b', value: '\\b', desc: 'Word boundary' },
]

// Quantifiers
const QUANTIFIER_BUTTONS = [
  { label: '*', value: '*', desc: '0 or more' },
  { label: '+', value: '+', desc: '1 or more' },
  { label: '?', value: '?', desc: '0 or 1' },
  { label: '{n}', value: '{3}', desc: 'Exactly n times' },
  { label: '{n,m}', value: '{2,5}', desc: 'Between n and m' },
]

// Anchors
const ANCHOR_BUTTONS = [
  { label: '^', value: '^', desc: 'Start of string' },
  { label: '$', value: '$', desc: 'End of string' },
]

Click a button, append to pattern. Simple:

const insertAtCursor = (value: string) => {
  setPattern(prev => prev + value)
}

2. Real-time Highlighting#

The core is looping through RegExp.prototype.exec():

function highlightMatches(text: string, pattern: string, flags: string) {
  const regex = new RegExp(pattern, flags.includes('g') ? flags : flags + 'g')
  const parts: { text: string; isMatch: boolean }[] = []
  let lastIndex = 0
  let match: RegExpExecArray | null

  while ((match = regex.exec(text)) !== null) {
    // Text before match
    if (match.index > lastIndex) {
      parts.push({ text: text.slice(lastIndex, match.index), isMatch: false })
    }
    // Matched text
    parts.push({ text: match[0], isMatch: true })
    lastIndex = match.index + match[0].length

    // Prevent zero-width match infinite loop
    if (match[0].length === 0) regex.lastIndex++
  }

  // Remaining text
  if (lastIndex < text.length) {
    parts.push({ text: text.slice(lastIndex), isMatch: false })
  }

  return parts
}

Render with highlighted matches:

{parts.map((part, i) =>
  part.isMatch ? (
    <mark key={i} className="bg-cyan-500/30 text-cyan-300">
      {part.text}
    </mark>
  ) : (
    <span key={i}>{part.text}</span>
  )
)}

3. The Zero-Width Match Trap#

The line if (match[0].length === 0) regex.lastIndex++ is critical.

Zero-width matches (like /\b/g for word boundaries) return empty strings. Without moving lastIndex forward, exec() stays at the same position forever:

const text = 'hello'
const regex = /\b/g
let match

// Infinite loop without protection
while ((match = regex.exec(text)) !== null) {
  console.log(match.index) // 0, 0, 0, 0, ...
}

4. Flag Toggles#

Regex flags change matching behavior:

  • g: Global (find all matches)
  • i: Case insensitive
  • m: Multiline (^ and $ match line start/end)
  • s: DotAll (. matches newlines)
  • u: Unicode mode

Toggle with checkboxes:

const toggleFlag = (flag: string) => {
  setFlags(prev => prev.includes(flag)
    ? prev.replace(flag, '')
    : prev + flag
  )
}

5. Preset Patterns#

Ready-to-use patterns for common cases:

const COMMON_PATTERNS = [
  { name: 'Email', pattern: '^[\\w.-]+@[\\w.-]+\\.\\w+$' },
  { name: 'URL', pattern: '^https?://[^\\s/$.?#].[^\\s]*$' },
  { name: 'Phone (CN)', pattern: '^1[3-9]\\d{9}$' },
  { name: 'IP Address', pattern: '^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$' },
  { name: 'Date', pattern: '^\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}$' },
  { name: 'ID Card', pattern: '^\\d{17}[\\dXx]$' },
  { name: 'Chinese', pattern: '[\\u4e00-\\u9fa5]' },
]

One click fills the pattern — great for learning reference.

Error Handling#

Invalid regex syntax throws in new RegExp(). Catch and display:

const { matches, error, isValid } = useMemo(() => {
  if (!pattern) return { matches: [], error: '', isValid: true }

  try {
    const regex = new RegExp(pattern, flags)
    const result = testText.match(regex)
    return { matches: result || [], error: '', isValid: true }
  } catch (err) {
    return { matches: [], error: (err as Error).message, isValid: false }
  }
}, [pattern, flags, testText])

Example error:

Invalid regular expression: /()/
Unterminated group

Performance#

For large text, use useMemo to cache results:

const highlightMatches = useMemo(() => {
  if (!pattern || !testText || !isValid) return null
  return computeHighlight(testText, pattern, flags)
}, [pattern, flags, testText, isValid])

For extremely large inputs, Web Workers can offload matching to a background thread. But in most cases, native regex is fast enough.

Edge Cases#

1. Escape Characters#

User types \d, but in a string that’s \\d:

const pattern = '\d'    // Actually just 'd'
const pattern = '\\d'   // Correct: \ and d

2. Literal Special Characters#

To match literal . or *, escape them:

const text = 'a.b*c'
const regex = /\.\*/  // Matches literal .*

The generator should provide an “escape” button.

3. Unicode Characters#

Matching Chinese or emoji requires the u flag:

const text = '你好👋'
const regex = /[\u4e00-\u9fa5]/gu  // Match Chinese
const regex2 = /\p{Emoji}/gu        // Match emoji (needs u flag)

Result#

Based on these ideas, I built: Regex Generator

Features:

  • Visual regex builder with buttons
  • Real-time match highlighting
  • Preset common patterns
  • Flag toggles
  • Quick syntax reference

Writing regex doesn’t have to be painful.


Related: Regex Tester | Regex Explainer