Regex to extract the text between two delimiters
To capture what sits between two delimiters, use a negated character class when the closing delimiter is one character and a lazy quantifier when it is longer. Neither handles nesting.
At a glance
- Single character delimiter
- \[([^\]]*)\], no backtracking
- Multi character delimiter
- start([\s\S]*?)end, lazy
- Across lines
- the dot needs the s flag, [\s\S] does not
- Nesting
- not expressible, the first closing delimiter always wins
- Getting every occurrence
- matchAll with the g flag, then read m[1]
- Escaping the delimiters
- brackets and braces need a backslash outside a class
In detail
When the closing delimiter is a single character, the negated class is the right answer and it is not close. /\[([^\]]*)\]/ consumes everything up to the first closing bracket and physically cannot cross it, so there is no backtracking to do and no way for the match to run away. It is also easier to read than the lazy alternative once you are used to the shape.
When the delimiter is longer than one character there is no class that can express it, and a lazy quantifier is the practical spelling. Wrapping the content in a lazy group between the two literal delimiters works, and bounding the quantifier as {0,2000}? rather than leaving it open puts a ceiling on how much work a malformed input can cause.
Crossing lines is a separate decision. A lazy dot will not cross a newline without the s flag, whereas [\s\S]*? crosses regardless. For a block delimited by markers on their own lines the second form is almost always what you want, and it says so in the pattern rather than depending on a flag set somewhere else.
Nesting is the limit worth internalising. Given "[a [b] c]" the bracket pattern captures "a [b", because the first closing bracket ends the match and there is no counter anywhere in the machinery to say otherwise. Matching balanced delimiters requires a stack, which is exactly the thing regular expressions do not have, so anything genuinely nested needs a small parser instead.
Examples
const s = "before [one] middle [two] after";
s.match(/\[(.*?)\]/g); // ["[one]", "[two]"]
[...s.matchAll(/\[([^\]]*)\]/g)].map((m) => m[1]); // ["one", "two"]The g flag gives you the whole match; matchAll plus m[1] gives you the contents without the delimiters.
"<a>\nx\n</a>".match(/<a>([\s\S]*?)<\/a>/)[1]; // "\nx\n"
"[a [b] c]".match(/\[([^\]]*)\]/)[1]; // "a [b"The nested case stops at the first closing bracket. No regular expression can count depth.
Things that catch people out
- Nesting is not expressible. The first closing delimiter always ends the match, so a nested structure comes back truncated rather than raising an error.
- A lazy dot will not cross a newline without the s flag, so a block pattern that works on one line silently returns nothing on two.
- Square brackets and braces are metacharacters outside a character class, so a pattern for them needs escaping even though the same characters are literal inside a class.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →Lazy quantifiers: *?, +?, ?? and {n,m}?
A trailing question mark makes a quantifier prefer the shortest match. It fixes the overreaching tag pattern, and a negated class often fixes it better still.
[^abc]: negated character sets that quietly cross newlines
A negated class matches one character that is not in the set, including line terminators. That is why a quoted string pattern runs across lines.
(...) capturing groups: numbering and what comes back
How capture groups are numbered, what a group that did not participate returns, and why quantifying a group throws away everything but the last pass.
The g flag: global matching and the lastIndex trap
What the g flag changes in a JavaScript regex, why a reused global pattern skips matches through lastIndex, and when matchAll is the safer choice.
The i flag: case insensitive matching
How the i flag folds case in literals and ranges, what it deliberately leaves alone, and the two non-ASCII characters it starts matching once u is on.
The m flag: multiline anchors
What multiline mode changes about ^ and $, which line terminators JavaScript counts, and why the m flag never makes the dot match a newline.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links