Building a Visual Regex Generator: From Zero to Match
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,\sare 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 insensitivem: 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