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.

pattern: [bhr]
one position, three allowed characters
‘b’
‘h’
‘r’
→ matches any single one of these
A bracket group describes one position. The engine tries each listed option there and accepts the first that fits.

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.

interactiveA set fills exactly one slot

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.

[0-5]
012345
[a-f]
abcdef
A hyphen between two characters expands to every code point between them, inclusive.

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 09 or an uppercase letter AF. 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.

interactiveRanges: grab hex color codes

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 codes 200c and 200d used 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:[^…].

[aeyo]
matches one of: a e y o
[^aeyo]
matches any character that is NOT a, e, y, or o
A leading caret negates the set: every character passes except the listed ones.

A few common ones:

  • pattern:[^aeyo] — any character except 'a', 'e', 'y', or 'o'.
  • pattern:[^0-9] — any character except a digit, the same as pattern:\D.
  • pattern:[^\s] — any non-whitespace character, the same as pattern:\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.

interactiveExcluding set: what's NOT a letter, digit, or space

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:

what you wrote: [𝐏𝐐]
engine sees:
55349
left half of 𝐏
56335
right half of 𝐏
 
55349
left half of 𝐐
56336
right half of 𝐐
Without the u flag, each astral character splits into two half-characters, so the set holds four items instead of two.

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.