Character Classes and Quantifiers in Regex

If literals and quantifiers are the alphabet of regex, character classes are its vocabulary. A class defined with square brackets matches exactly one character out of a set you choose, and quantifiers decide how many such characters to consume. Master these two and you can describe almost any real-world token — identifiers, numbers, codes, slugs — precisely.
Custom classes and ranges
Inside square brackets you list allowed characters: [aeiou] matches any single vowel. A hyphen creates a range, so [a-z] is any lowercase letter, [A-Za-z0-9] is any alphanumeric character, and [0-9] is the same as \d. You can mix ranges and single characters freely: [A-Fa-f0-9] matches a hexadecimal digit, which is why a hex colour looks like #[A-Fa-f0-9]{6}.
A caret as the first character negates the class: [^0-9] matches anything that is not a digit, and [^\s] matches any non-whitespace character. Inside a class most metacharacters lose their special meaning — a dot in [.] is a literal dot — which often makes classes simpler than escaping everything outside them.
Quantifiers and their ranges
Quantifiers attach to the item just before them. The three shorthands are * (zero or more), + (one or more) and ? (zero or one). For exact control use braces: {3} means exactly three, {2,} means two or more, and {2,5} means between two and five. So [A-Z]{2,3}\d{4} matches a code of two-or-three capitals followed by four digits.
A quantifier applies to a single element unless you group first. a{3} matches aaa, but to repeat a whole sequence you wrap it in parentheses: (ab){3} matches ababab. This is the first place where grouping and quantifiers meet, and it trips up almost everyone once.
Greedy, lazy and catastrophic
By default quantifiers are greedy: they consume as much as possible, then give characters back until the rest of the pattern fits. Against <b>one</b> <b>two</b>, the pattern <b>.*</b> greedily matches the whole line. Add a ? to make the quantifier lazy — <b>.*?</b> — and it matches the shortest possible span, stopping at the first </b>. Choosing greedy or lazy is often the difference between a pattern that works and one that grabs too much.
Beware catastrophic backtracking: nesting quantifiers like (a+)+ over text that almost matches can make the engine explore an exponential number of paths and appear to hang. Keep classes specific, avoid overlapping quantifiers, and test on realistic input in the tester so you notice a slow pattern before it reaches production.
Shorthand classes and their Unicode blind spot
Three shorthands cover most everyday work: `\d` for a digit, `\w` for a word character and `\s` for whitespace, each with an uppercase negation — `\D`, `\W`, `\S`. It is worth knowing exactly what they expand to. In JavaScript `\d` is only `[0-9]` and `\w` is only `[A-Za-z0-9_]`, so neither sees Cyrillic, Greek, accented Latin or Arabic-Indic digits. `\s` is the generous one, covering spaces, tabs, newlines and a range of Unicode separators.
The fix is the `u` flag and Unicode property escapes. `\p{L}` matches any letter in any script, `\p{Nd}` any decimal digit, `\p{Script=Cyrillic}` a specific script, and each has a negated form `\P{...}`. So a name field that should accept `Ольга` and `Zoë` wants `^\p{L}+$` with the `u` flag rather than `^\w+$`. The cost is one flag; the benefit is a pattern that behaves the same for every user.
Bounding repetition before it bounds you
Catastrophic backtracking is not a curiosity — it is a denial-of-service class known as ReDoS. The shape to watch for is a quantifier wrapped around something that is itself repeatable and ambiguous: `(a+)+$`, `(\s*\w+)*$`, `(.*,)*`. On a string of thirty characters that ultimately fails to match, the engine can explore billions of ways to split the input before giving up, and a single request can pin a CPU core for minutes.
Other engines offer escape hatches JavaScript does not: atomic groups `(?>...)` and possessive quantifiers `a++` in PCRE, Java and Ruby tell the engine never to give characters back. RE2, used by Go and available to Rust and Python through libraries, guarantees linear time by refusing backreferences and lookaround entirely. In JavaScript the practical defences are to avoid nesting quantifiers, make each alternative start with a distinct character, and replace an unbounded `*` with a concrete ceiling such as `{1,64}`.
The takeaway
Character classes match one of a set — use ranges and a leading caret to negate. Quantifiers control repetition and default to greedy; add ? for lazy. Group before quantifying a sequence, and watch for catastrophic backtracking.
Try this pattern live with match highlighting.
More guides

Regex Basics: How Pattern Matching Works
A regular expression is a tiny language for describing text patterns. How the engine scans a string, and the metacharacters you meet on day one.

Capture Groups and Backreferences in Regex
Parentheses do more than group — they capture. Learn numbered and named groups, non-capturing groups, backreferences, and how $1 powers find-and-replace.

Lookahead & Lookbehind: Matching by Context
Lookaround checks what surrounds a position without consuming it. Positive and negative lookahead and lookbehind, with password and formatting examples.