RegExp v and d Flags
A character class like [a-z] has always been a flat bag of characters. You can list ranges, you can negate the whole thing with ^, and that is about the end of it. There is no way to say “letters, but not vowels” or “Greek script, but only the letters” without spelling out every excluded character by hand. And once you do get a match, the result tells you what matched but is stingy about where — you get the matched text, one lonely .index for the whole thing, and nothing about each capture group.
Two flags close those gaps. The v flag turns character classes into a small algebra: union, intersection, difference, and even sets of whole strings. The d flag attaches precise [start, end] positions to every part of a match. They solve unrelated problems, but both are recent, both are worth reaching for, and both are safe to use in 2026.
The v flag: character classes that do algebra
The v flag is a successor to the older u (Unicode) flag. Both make a regex Unicode-aware — matching by code point rather than by UTF-16 code unit, and enabling \p{…} property escapes. But v goes further. It reinterprets the inside of […] as a proper set that you can combine with other sets.
The catch, stated up front: u and v are mutually exclusive. They interpret the same source in incompatible ways, so a regex carrying both throws a SyntaxError immediately.
new RegExp("[a-z]", "uv"); // SyntaxError: Invalid flags supplied to RegExp constructor
You check for the v flag the same way you check any flag — through the boolean accessor on the regex:
const re = /[\p{Letter}]/v;
re.unicodeSets; // true
re.flags; // "v"
Everything a u-mode regex could do, a v-mode regex still does. The new powers all live inside the square brackets.
Union: the familiar behaviour, now spelled out
Listing things inside a class has always meant “any of these” — that is union. Under v the idea is the same, but the syntax is looser and reads better: you can put ranges, property escapes, nested classes, and (new) string literals side by side.
/[\p{Emoji}\p{ASCII}0-9]/v; // emoji OR ASCII OR a digit
Nothing surprising yet. The interesting part is that a class is now a value you can subtract from and intersect with.
Intersection with &&
A&&B inside a class matches a character only if it belongs to both A and B. This is exactly the Venn-diagram overlap you would draw for two circles.
// letters that are also lowercase ASCII
/[\p{Letter}&&[a-z]]/v.test("k"); // true
/[\p{Letter}&&[a-z]]/v.test("K"); // false — uppercase isn't in [a-z]
/[\p{Letter}&&[a-z]]/v.test("5"); // false — not a letter
// ASCII whitespace only, not the exotic Unicode spaces
const asciiSpace = /[\p{White_Space}&&\p{ASCII}]/v;
If that overlap picture reminds you of Set.prototype.intersection, it should — it is the same operation, one level down at the character-class layer. The Set methods do it for runtime collections; v does it for the alphabet a pattern accepts.
Difference with --
A--B matches characters in A that are not in B. This is the “letters, but not vowels” case that used to force you to enumerate consonants.
// any letter except the five ASCII vowels
const consonantish = /[\p{Letter}--[aeiou]]/v;
consonantish.test("b"); // true
consonantish.test("a"); // false
// every Unicode decimal digit that isn't a plain ASCII 0-9
/[\p{Decimal_Number}--[0-9]]/v.test("٤"); // true (Arabic-Indic four)
/[\p{Decimal_Number}--[0-9]]/v.test("4"); // false
Type a single character below and watch three v-mode classes decide whether it belongs. Each row is one set expression; edit the code to invent your own intersection or difference.
String literals with \q{…}
Here is the genuinely new capability. Until now a character class matched exactly one character. The \q{…} escape lets a class contain whole strings as elements, separated by |.
/[\q{a|bc|def}]/v; // matches the string "a", or "bc", or "def"
On its own that looks like a clumsy way to write (?:a|bc|def). The payoff is that these multi-character elements now participate in the set algebra. You can subtract a specific emoji sequence, or intersect a property set against a list of allowed strings.
// national-flag emoji, but not the flag of one specific region
const flags = /[\p{RGI_Emoji_Flag_Sequence}--\q{🇦🇶}]/v; // everything except Antarctica
\q{…} is where v stops being “a nicer u” and becomes something u simply cannot express. A u-mode class is a set of code points; a v-mode class is a set of strings, most of which happen to be one character long.
The demo below anchors a \q{…} set so the whole input has to be a single element of it. Notice that "do" fails even though it is a prefix of "dog" — a string element is all-or-nothing, and the difference operator lets you punch one word back out.
Why v is emoji-safe
Many emoji are not single code points. A flag is two regional-indicator symbols; 👨👩👧 is several people glyphs stitched together with zero-width joiners. Under u, a bracket class treats each of those underlying code points as a separate class member, which quietly breaks negation and intersection around emoji. Under v, the property \p{RGI_Emoji} matches the recommended-for-interchange emoji as whole units, sequences included, because the class can hold multi-code-point strings.
Escaping is stricter under v
Because &, -, and a few other characters now carry meaning inside a class, v mode is fussier about literals. A doubled punctuation like &&, !!, ##, or ~~ inside a class is reserved and must be escaped if you mean it literally. The upside is fewer silent surprises; the cost is that a pattern copied from a u regex might need a backslash or two.
/[a&&b]/v; // SyntaxError — && is the intersection operator
/[a\&\&b]/v.test("&"); // true — escaped, so a literal ampersand
The d flag: exact positions for every group
Switch problems. A match under a normal regex hands back the matched text and a single .index marking where the whole match started. If you want to know where a specific capture group landed — to highlight it, to splice around it, to map it back to a source location — you are on your own, usually re-searching the string by hand.
The d flag makes the engine record that for you. It adds an indices property to the match array, and hasIndices reports whether the flag is on.
const re = /(\d{4})-(\d{2})/d;
const m = re.exec("ship 2026-07 now");
m[0]; // "2026-07" — the full match
m.index; // 5 — where the full match starts
m.indices[0]; // [5, 12] — [start, end] of the full match
m.indices[1]; // [5, 9] — group 1, the year
m.indices[2]; // [10, 12] — group 2, the month
re.hasIndices; // true
Each entry in indices is a two-element array [start, end], where end is exclusive — the same half-open convention as String.prototype.slice. So str.slice(...m.indices[1]) gives you exactly that group’s text, no arithmetic required. indices[0] is the whole match; indices[1] onward line up with m[1], m[2], … one-to-one.
Because every string method that matches internally calls exec, the flag flows through them too. String.prototype.matchAll with a /d regex yields match objects that each carry their own indices, which is what makes it practical for scanning a whole document.
Named groups land on indices.groups
If your pattern uses named capture groups, indices grows a groups object that mirrors match.groups, but with position pairs instead of text.
const re = /(?<year>\d{4})-(?<month>\d{2})/d;
const m = re.exec("2026-07");
m.groups.year; // "2026"
m.indices.groups.year; // [0, 4]
m.indices.groups.month; // [5, 7]
Unmatched optional groups report undefined
Positions only exist for groups that actually participated. An optional group that did not match gives undefined in indices, exactly as it gives undefined in the match array — so guard before spreading.
const re = /(a)|(b)/d;
const m = re.exec("a");
m.indices[1]; // [0, 1] — the (a) branch matched
m.indices[2]; // undefined — the (b) branch never ran
A realistic use: precise highlighting
The classic reason to reach for d is turning a match into styled output without re-scanning. Say you want to wrap the month portion of every date in a <mark>. With indices you know its exact span, so you can rebuild the string in one pass.
function highlightMonths(text) {
const re = /(?<year>\d{4})-(?<month>\d{2})/dg;
let out = "";
let cursor = 0;
for (const m of text.matchAll(re)) {
const [start, end] = m.indices.groups.month;
out += text.slice(cursor, start); // text before the month
out += `<mark>${text.slice(start, end)}</mark>`;
cursor = end;
}
return out + text.slice(cursor);
}
highlightMonths("from 2026-07 to 2027-01");
// "from 2026-<mark>07</mark> to 2027-<mark>01</mark>"
No second search, no counting characters, no fragile assumptions about where inside the match the group sits. The engine already knew; the d flag just let it tell you. This is the pattern behind syntax highlighters, linters that underline a sub-token, and any tool that maps a match back to a location in source.
Here that same function runs live. Type any text containing YYYY-MM dates and only the month digits get wrapped, using indices.groups.month to find the exact span.
Support and when to use them
Both flags are shipped and standardized — no polyfill, no transpile step for the language features themselves (though a build tool can still down-level v-flag syntax if you target ancient engines).
The d flag (ES2022) landed in Chrome 90 and Firefox 88 in April 2021 and in Safari 15 that September, so it has been comfortably deployable for years. The v flag (ES2024) arrived with Chrome 112 in spring 2023, Firefox 116 that August, and Safari 17 in September 2023 — Baseline “newly available” from that point, and by 2026 it has aged past the widely-available threshold. Use v freely in evergreen and current-mobile targets; if you must support a browser older than late 2023, keep a u-flag fallback or transpile.
Summary
v(unicodeSets, ES2024) upgradesu: character classes become sets you can combine. It is mutually exclusive withu— using both throws.- Inside a
vclass:&&is intersection,--is difference, adjacency is union, and\q{…}introduces multi-character string elements separated by|. - Mix operators only across nesting levels:
[[\p{L}&&[a-z]]--[aeiou]], one operator per bracket level. Doubled punctuation like&&must be escaped to be literal. \p{RGI_Emoji}undervmatches emoji sequences as whole units, whichu-mode classes cannot do because they see individual code points.d(hasIndices, ES2022) addsmatch.indices:indices[0]is the whole match’s[start, end], andindices[1..]line up with the capture groups.endis exclusive, soslice(...pair)gives the text.- Named groups appear on
indices.groups, keyed likematch.groups. Groups that did not participate areundefined— guard before spreading intoslice. - Both flags are standardized and shipped:
dhas been widely available since 2021,vsince 2023. Reach forvas the default in new code.