Matching HTML with Regular Expressions
Regex and HTML have a famously complicated relationship. Every developer eventually needs to strip tags, extract a link, or grep for a specific element — and regex looks like the fastest tool for the job. It often is. But regex fundamentally cannot parse HTML correctly in all cases because HTML is a nested language and regex has no memory of nesting. This guide covers what regex CAN do reliably, what patterns to use, and when to switch to a proper parser.
The Core Patterns
Match any HTML tag
/<[^>]+>/g // opens with <, closes with >, no > in between
/<\/?[a-z][a-z0-9]*\b[^>]*>/gi // stricter — must start with a letterThe stricter pattern refuses to match nonsense like <123> or bare <>. It also handles closing tags via the optional \/?.
Match a specific tag with content
// <p>...anything...</p> — the [\s\S]*? is a "match anything lazily" trick
/<p[^>]*>([\s\S]*?)<\/p>/gi
// <a> tag with its href
/<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi
// <img> tag with its src (self-closing, no closing tag)
/<img\s[^>]*src\s*=\s*["']([^"']+)["'][^>]*\/?>/giExtract just the tag name
/<([a-z][a-z0-9]*)\b/gi // captures p, div, span, h1, etc.Strip all attributes but keep the tag
// Turns <p class="foo" id="bar">text</p> into <p>text</p>
html.replace(/<([a-z][a-z0-9]*)\b[^>]*>/gi, '<$1>');Language-Specific Usage
JavaScript — strip all HTML tags
function stripHtml(html) {
return html
.replace(/<[^>]+>/g, '') // remove tags
.replace(/ /g, ' ') // decode common entities
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/\s+/g, ' ') // collapse whitespace
.trim();
}
stripHtml('<p class="foo">Hello <b>world</b>!</p>');
// → 'Hello world!'
// SAFER alternative — use the DOM
function stripHtmlSafe(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
return doc.body.textContent || '';
}Python
import re
TAG_RE = re.compile(r'<[^>]+>')
def strip_html(html: str) -> str:
return re.sub(TAG_RE, '', html)
# Better — use BeautifulSoup for anything non-trivial
from bs4 import BeautifulSoup
def strip_html_safe(html: str) -> str:
return BeautifulSoup(html, 'html.parser').get_text(' ', strip=True)PHP
// PHP has a purpose-built function — use it
$plain = strip_tags($html);
// Allow certain tags through
$plain = strip_tags($html, '<b><i><a>');
// Or regex if you need custom logic
$plain = preg_replace('/<[^>]+>/', '', $html);When Regex Fails
Quoted > inside attributes
The pattern <[^>]+> stops at the first >. But this is valid HTML:
<a title="1 > 0">click</a>The regex matches <a title="1 > and stops — cutting the tag in half. A stricter pattern that handles quoted strings exists but is much longer:/<[a-z][^>"]*(?:"[^"]*"[^>"]*)*>/gi.
Comments and CDATA
HTML comments <!-- ... --> and CDATA blocks are matched by generic tag patterns, which almost never what you want. Always strip them first:
html
.replace(/<!--[\s\S]*?-->/g, '') // strip comments
.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, ''); // strip CDATANested tags of the same type
Regex cannot correctly match nested <div><div></div></div> because it has no way to track depth. The lazy match ([\s\S]*?) stops at the first closing tag, cutting nested structures in half. This is the classic case for switching to a DOM parser.
Script and style content
<script> and <style> contents can contain characters that look like HTML tags but are not. Regex will happily match them, which is almost certainly wrong. Strip these blocks first:
html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '');HTML Regex Cheatsheet
| Task | Pattern | Notes |
|---|---|---|
| Strip all tags | /<[^>]+>/g | Clean HTML only |
| Match specific tag | /<p[^>]*>([\s\S]*?)<\/p>/gi | Lazy match, no nesting |
| Extract link URLs | /href="([^"]+)"/gi | Quick and dirty ✅ |
| Remove comments | /<!--[\s\S]*?-->/g | Always strip first |
| Strip attributes | /<([a-z]+)\b[^>]*>/ → <$1> | Keeps tags, removes attrs |
Testing Your HTML Regex
Use the live Regex Tester with these HTML snippets:
- Simple:
<p>hello</p> - With attributes:
<a href="/x" class="btn">click</a> - Self-closing:
<br/>,<img src="x.png"/> - Nested:
<div><p><strong>bold</strong></p></div> - Trap: > in attribute:
<a title="1 > 0">x</a> - Comment:
<!-- do not match this -->