Regex Capture Groups Explained: $1, $&, and $<name> (2026)
Understand regex capture groups, backreferences, and the $1, $&, and $<name> replacement tokens — with copy-paste JavaScript examples that actually work.
Regex Capture Groups Explained: $1, $&, and $
You wrote a regex that matches. Now you need to pull a piece out of the match, or rewrite the text around it — and suddenly you are staring at a wall of $1, \1, $&, and $<name> that all look interchangeable but behave differently. Capture groups are the bridge between "the pattern matched" and "here is the part I want." This guide untangles every token, with runnable JavaScript you can paste straight into a console.
TL;DR
- A capture group is
(...); groups are numbered left-to-right by opening parenthesis. - Inside the pattern,
\1is a backreference; in the replacement,$1inserts the captured text. $&inserts the whole match;$<name>inserts a named group.- Use
(?:...)for a group you want to match but not capture. $1printing literally? Your pattern is a string, not aRegExp.
What is a capture group in regex?
A capture group is any sub-pattern wrapped in parentheses. Beyond grouping for repetition or alternation, parentheses tell the engine to remember the slice of text that part matched, so you can read it back later through match[1], a backreference, or a replacement token.
How regex numbers capture groups
Groups are numbered by the position of their opening parenthesis, left to right, starting at 1. Group 0 is special: it always refers to the entire match. Per the MDN guide on groups and backreferences, nesting follows the same rule — the outer paren opens first, so it gets the lower number.
const re = /(\d{4})-((\d{2})-(\d{2}))/;
"2026-06-27".match(re);
// [0] "2026-06-27" full match
// [1] "2026" first opening paren
// [2] "06-27" second opening paren
// [3] "06", [4] "27"
Non-capturing groups: (?:...)
If you only need parentheses for grouping — not for extraction — use (?:...). It does not consume a group number, which keeps your $1/$2 references stable when you later add or remove a plain group.
"catdog".match(/(?:cat)(dog)/);
// [1] is "dog", not "cat"
Named capture groups: (?...)
Numbered groups get fragile fast — insert one paren near the front and every downstream number shifts. Named groups fix that. Write (?<name>...) and read the result from match.groups.name.
const re = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/;
const m = "2026-06-27".match(re);
m.groups.y; // "2026"
m.groups.m; // "06"
What is the difference between \1 and $1 in regex?
This is the single most common point of confusion, and it comes down to where the token lives. \1 belongs inside the pattern. $1 belongs in the replacement string. They reference the same captured text but do completely different jobs.
Backreferences inside the pattern: \1 and \k
A backreference re-matches whatever a group already captured, in the same pattern. \1 means "the exact text group 1 matched a moment ago." It is how you detect doubled words or matching quote characters.
// Find a word repeated twice in a row
/\b(\w+)\s+\1\b/.test("the the"); // true
/\b(\w+)\s+\1\b/.test("the cat"); // false
Named groups have a named backreference, \k<name>:
/(?<q>['"]).*?\k<q>/.test(`"quoted"`); // true
Reference variables in the replacement: $1, $2
Once the match is done, the replacement string uses $ tokens, not backslashes. $1 inserts the text captured by group 1 into the output. According to MDN's String.prototype.replace() reference, $n accepts any positive integer less than 100.
"Maria Cruz".replace(/(\w+)\s(\w+)/, "$2, $1");
// "Cruz, Maria"
A quick table of every $ replacement token
These are all the special tokens a replacement string understands. Recreate them in your head as "insert X" and they stop being cryptic.
| Token | Inserts |
|---|---|
$$ |
A literal dollar sign |
$& |
The whole matched substring |
$` |
Everything before the match |
$' |
Everything after the match |
$n |
The nth captured group (1–99) |
$<name> |
A named captured group |
How to swap two words in a JavaScript regex
The name-swap above is the canonical demo, but the same $1/$2 mechanic powers most real text rewriting: reordering date parts, reformatting phone numbers, or turning LastName, FirstName into FirstName LastName.
The classic reorder
const reISO = /(\d{4})-(\d{2})-(\d{2})/;
"2026-06-27".replace(reISO, "$3/$2/$1");
// "27/06/2026"
$& : insert the whole match
When you want to keep the entire match and wrap something around it, you do not need a capture group at all — $& is the whole match.
"Total: 42 items".replace(/\d+/, "[$&]");
// "Total: [42] items"
One sharp edge worth memorizing: $& only means "the match" inside a replacement string. Calling "$&".toLowerCase() in your own code does nothing useful, because $& is just two ordinary characters until replace() interprets it. MDN flags this exact trap on the replace() page.
$ : named groups in replacements
Named groups carry into the replacement too, via $<name>. This reads far better than counting parentheses six months later.
const re = /(?<first>\w+)\s(?<last>\w+)/;
"Maria Cruz".replace(re, "$<last>, $<first>");
// "Cruz, Maria"
Capture group gotchas that bite developers
Most "my regex replacement is broken" bugs trace back to three specific behaviors. None of them are bugs in the engine — they are documented rules that are easy to forget.
Why does $1 print literally instead of the captured value?
The $n and $<name> tokens only work when the pattern is a RegExp object. Pass a plain string and there are no groups to reference, so the token is emitted verbatim:
"foo".replace("f", "$1"); // "$1oo" — string pattern
"foo".replace(/(f)/, "$1"); // "foo" — real group
The same literal fallback happens if you reference a group number that does not exist — "foo".replace(/(f)/, "$2") yields "$2oo" because there is no group 2.
Why unmatched groups become empty, not undefined
If a group exists but did not participate in the match — common with alternation — its $n resolves to an empty string, not undefined:
"foo".replace(/(f)|(g)/, "$2"); // "oo"
Group 2 (g) never matched, so $2 is empty and only the rest of the string survives. In a replacer function, that same unmatched alternative arrives as undefined — a subtle but real difference between the string and function paths.
Counting groups when you nest parentheses
Every opening parenthesis you add shifts the numbering of everything after it. This is the strongest argument for named groups or (?:...): a pattern that uses $<year> does not silently break when a colleague wraps part of it in a new group. When you are building something gnarly, validate it live in the iKit Regex Tester so you can watch the group panel update as you type instead of guessing the numbers.
In a replacer function, $1 becomes p1
For anything beyond simple substitution — math, case changes, lookups — pass a function instead of a string. The captured groups arrive as positional arguments.
The argument order: match, p1…pN, offset, string, groups
MDN specifies the signature as (match, p1, p2, …, pN, offset, string, groups). Here match is $&, each pN is $n, offset is where the match starts, string is the whole input, and groups (present only if you used named groups) is an object of named captures.
const re = /(\d{4})-(\d{2})-(\d{2})/;
"2026-06-27".replace(re, (m, y, mo, d) => {
return `${d}.${mo}.${y}`;
});
// "27.06.2026"
Two rules to keep in mind: the special $ tokens do not apply to strings returned from your function — return plain text. And if you build a generic replacer with rest params, remember the trailing arguments shift when named groups are present, because groups is appended after string. The behavior here is defined by the ECMAScript text-processing spec maintained at tc39.es/ecma262.
A common real-world use: normalize identifiers. If you are validating UUIDs, a capture-group pattern pairs well with the structure described in our UUID generator, and you can diff before/after output in the iKit Diff Checker to confirm a bulk rewrite did exactly what you intended.
References
- Groups and backreferences — JavaScript | MDN — group numbering, named groups, and
\1/\k<name>backreference syntax. - String.prototype.replace() — JavaScript | MDN — the full
$$,$&,$`,$',$n,$<Name>token table and string-vs-RegExp literal rules. - Capturing group: (...) — JavaScript | MDN — confirmation of opening-parenthesis numbering and non-capturing
(?:...). - ECMAScript Language Specification — String.prototype.replace — normative replacer-function argument order.
Related on iKit
- Test any regex pattern live in the browser — the companion to this guide: watch capture groups resolve as you edit the pattern.
- The 2026 regex cheatsheet of patterns you'll reuse weekly — quick reference for the tokens that feed your capture groups.
- Email regex patterns compared, strict to loose — real patterns that lean heavily on grouping and alternation.
- Match a URL with regex and the edge cases most patterns miss — capture host, path, and query into separate groups.
- JavaScript vs PCRE vs Python regex: why it mysteriously fails — backreference and named-group syntax differs across engines.
- Regex lookahead vs lookbehind with five real examples — non-capturing assertions that pair naturally with capture groups.
Related posts
Ignore Whitespace in Git Diff: When It Hides Bugs (2026)
Ignore whitespace in a diff and you cut formatting noise — but the same flag can hide real bugs in Python, YAML, and Makefiles. Here's when to use it.
Side-by-Side vs Unified Diff: How to Compare Text (2026)
Side-by-side and unified diff show the same edits two ways. Learn how each format reads, what the @@ hunk header means, and when to pick which.
Regex Lookahead vs Lookbehind: 5 Real Examples (2026)
Regex lookahead vs lookbehind, explained with five production patterns: password rules, thousands separators, price parsing, and camelCase splits.