Sets and ranges [...]
Square brackets […] are how you say “at this one spot, match any character from this list.” List a few characters, list a range, drop in a shorthand class — all inside the brackets, all describing a single position in the text.
That last part trips people up, so hold onto it: however many characters you pack into the brackets, they still stand for exactly one character in the match.
Sets
A plain list of characters in brackets is called a set. pattern:[bhr] matches any one of 'b', 'h', or 'r'. Order inside the brackets doesn’t matter, and repeats are harmless.
You mix a set freely with ordinary characters in the rest of the pattern:
// find [b or h], and then "at"
alert( "Bat, hat, and a flat cat".match(/[bh]at/gi) ); // "Bat", "hat"
The pattern:[bh] fills one slot — either b or h — and at follows literally. With the pattern:i flag the match is case-insensitive, so Bat (capital B) counts too.
Because the set is a single position, a pattern that looks like it should be flexible can still miss:
// find "B", then [a or e], then "rd"
alert( "Beard".match(/B[ae]rd/) ); // null, no matches
Read the pattern slot by slot:
pattern:B,- then one of the letters
pattern:[ae], - then
pattern:rd.
That spells Bard or Berd — four characters. The string "Beard" has ea between the B and the rd, which is two characters where the pattern allows one. No match.
Type into the tester below to watch a set match one slot at a time. Start with [bh]at against the text, then try [ae]rd — you’ll see it highlight Bard/Berd shapes but skip Beard, exactly as the reasoning above predicts. Edit the string and the pattern freely.
Ranges
Listing every character gets tedious, so brackets also accept a range: two endpoints joined by a hyphen.
pattern:[a-z] is any single character from a to z. pattern:[0-5] is any digit from 0 to 5. The range is defined by the underlying character codes, so a-z works because a through z sit consecutively in the code table.
You can stack several ranges in the same brackets. Here we look for "x" followed by two characters that are each a digit or a letter from A to F:
alert( "Register 0x7E".match(/x[0-9A-F][0-9A-F]/g) ); // x7E
pattern:[0-9A-F] holds two ranges side by side: match a character that is a digit 0–9 or an uppercase letter A–F. That is exactly the hexadecimal digit set, which is why this pulls 7E out of 0x7E.
Want lowercase hex digits too? Add the a-f range — pattern:[0-9A-Fa-f] — or just tack on the pattern:i flag and let case-insensitivity handle it.
Here’s a range in action: this pattern pulls three-digit hex color codes out of the text. Try widening the range (drop the a-f, or shrink it to [0-9A-C]) and watch which codes still light up.
Character classes live in brackets too
Inside […] you can drop shorthand classes right next to characters and ranges.
Say you want a word character pattern:\w or a hyphen pattern:-. The set is pattern:[\w-]. (The hyphen sits at the end, where it can’t start a range, so it means a literal hyphen — more on that below.)
You can combine classes as well. pattern:[\s\d] reads as “a whitespace character or a digit.”
Example: a multi-language \w
Since pattern:\w expands to pattern:[a-zA-Z0-9_], it is ASCII-only. It won’t match Chinese characters, Cyrillic letters, accented vowels, or anything outside that Latin-plus-digits range.
To match “word characters in any language,” build your own set out of Unicode property classes:
pattern:[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]
Piece by piece, that set gathers characters carrying these Unicode properties:
Alphabetic(Alpha) — letters in any script,Mark(M) — combining accents and diacritics,Decimal_Number(Nd) — digits,Connector_Punctuation(Pc) — the underscore'_'and similar joiners,Join_Control(Join_C) — the two control codes200cand200dused to shape ligatures, for example in Arabic.
let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;
let str = `Yo 汉字 88`;
// finds all letters and digits:
alert( str.match(regexp) ); // Y,o,汉,字,8,8
Note the pattern:u flag — Unicode property classes only work when it’s present. Tune the set as you like: add properties, remove ones you don’t want. Property classes get the full treatment in Unicode: flag “u” and class \p{…}.
Excluding ranges
Put a caret pattern:^ right after the opening bracket and the whole set flips meaning: it now matches any character except the ones listed. These are “excluding” (negated) sets, written pattern:[^…].
A few common ones:
pattern:[^aeyo]— any character except'a','e','y', or'o'.pattern:[^0-9]— any character except a digit, the same aspattern:\D.pattern:[^\s]— any non-whitespace character, the same aspattern:\S.
This example grabs every character that is not a letter, a digit, or a space:
alert( "nadia15@webmail.io".match(/[^\d\sA-Z]/gi) ); // @ and .
With the pattern:i flag, pattern:A-Z covers both cases, so the excluded set is “digits, whitespace, and letters.” What’s left in that email string is the @ and the ..
The demo below runs that exact negated set and shows the leftovers as chips. Type any string — every character that is not a letter, digit, or space gets picked out. It’s a quick way to see the punctuation and symbols hiding in a piece of text.
Escaping inside […]
Outside brackets, matching a literal special character means escaping it: pattern:\. for a dot, pattern:\\ for a backslash, and so on. Inside brackets the rules relax a lot, because most metacharacters lose their power there.
Inside […], the vast majority of special characters stand for themselves:
pattern:. + ( )never need escaping.- A hyphen
pattern:-is literal at the very start or very end of the set (anywhere it can’t form a range). - A caret
pattern:^only carries special meaning at the very start; elsewhere it’s literal. - The closing bracket
pattern:]always needs escaping when you want to match it, since an unescaped one would end the set.
The rule in one line: everything is allowed raw except where it would mean something to the brackets themselves.
So a dot inside brackets is just a dot. pattern:[.,] matches one character — either a dot or a comma — not “any character or a comma.”
Here’s a set of literal punctuation, no escaping needed:
// No need to escape
let regexp = /[-().^+]/g;
alert( "8 + 5 - 2".match(regexp) ); // Matches +, -
The - sits at the front (literal), the ^ isn’t at the front (literal), and . ( ) + never bite inside brackets. Only + and - actually appear in the string, so those are what come back.
If escaping everything “just to be safe” helps you sleep, it does no harm — the extra backslashes are simply ignored where they aren’t needed:
// Escaped everything
let regexp = /[\-\(\)\.\^\+]/g;
alert( "8 + 5 - 2".match(regexp) ); // also works: +, -
Ranges and the flag “u”
Characters above the basic 2-byte range — mathematical symbols, emoji, some historical scripts — are stored as surrogate pairs: two code units glued together. A regexp only treats such a pair as a single character when you add the pattern:u flag.
Try matching pattern:[𝐏𝐐] against the string subject:𝐏 without the flag:
alert( '𝐏'.match(/[𝐏𝐐]/) ); // shows a strange character, like [?]
// (the search was performed incorrectly, half-character returned)
The result is broken because, without pattern:u, the engine has no notion of surrogate pairs. It reads [𝐏𝐐] as four separate code units, not two characters:
You can see those four code units directly:
for(let i=0; i<'𝐏𝐐'.length; i++) {
alert('𝐏𝐐'.charCodeAt(i)); // 55349, 56335, 55349, 56336
};
That’s why the match returns the lone left half of 𝐏 — the engine matched a single code unit, half of a real character.
Add the pattern:u flag and the engine treats each pair as one unit:
alert( '𝐏'.match(/[𝐏𝐐]/u) ); // 𝐏
Ranges hit the same wall, and there the failure is louder. Consider [𝐏-𝐐] without the flag:
'𝐏'.match(/[𝐏-𝐐]/); // Error: Invalid regular expression
Since each character is seen as two code units, the engine reads the range as [<55349><56335>-<55349><56336>] — it takes the code unit on each side of the hyphen and tries to build a range 56335-55349. The start code 56335 is greater than the end 55349, which is not a valid range, so it throws.
With pattern:u, the endpoints are whole characters again and the range works:
// look for characters from 𝐏 to 𝐑
alert( '𝐐'.match(/[𝐏-𝐑]/u) ); // 𝐐
The rule of thumb: any time your set or range reaches into astral characters — emoji, mathematical letters, rare scripts — reach for the pattern:u flag first.