iKit
Technical · 9 min read ·

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 $<name> (2026)

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, \1 is a backreference; in the replacement, $1 inserts the captured text.
  • $& inserts the whole match; $<name> inserts a named group.
  • Use (?:...) for a group you want to match but not capture.
  • $1 printing literally? Your pattern is a string, not a RegExp.

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

Related on iKit

Related posts