Alternation is regexp-speak for a plain “OR”. You give it several options, and a match succeeds if any one of them fits at the current position.
The operator is a single vertical bar: pattern:|. Read pattern:A|B|C as “A, or B, or C”.
Say you want to spot mentions of a few drinks in text: tea, cola, milk, or lemonade. A pattern that covers all four:
tea|cola|milk|lemon(ade)?
The pattern:(ade)? tail is the clever bit. It makes ade optional, so lemon(ade)? matches both lemon and lemonade with one branch.
let regexp = /tea|cola|milk|lemon(ade)?/gi;let str = "First TEA arrived, then COLA, then Lemonade";alert( str.match(regexp) ); // 'TEA', 'COLA', 'Lemonade'
Type a sentence and watch each alternative light up wherever it fits. Notice how lemon(ade)? catches both lemon and lemonade.
interactiveHighlight the drink mentions
Live example — Highlight the drink mentions
HTML
<label for="txt">Text to scan</label>
<textarea id="txt" rows="3">First TEA arrived, then COLA, then Lemon and later Lemonade.</textarea>
<button id="go">Highlight matches</button>
<div id="out" class="out"></div>
<div id="list" class="list"></div>
const re = /tea|cola|milk|lemon(ade)?/gi;
const txt = document.getElementById("txt");
const out = document.getElementById("out");
const list = document.getElementById("list");
function esc(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function render() {
const text = txt.value;
let html = "";
let last = 0;
let m;
const found = [];
re.lastIndex = 0;
while ((m = re.exec(text)) !== null) {
html += esc(text.slice(last, m.index));
html += "<mark>" + esc(m[0]) + "</mark>";
last = m.index + m[0].length;
found.push(m[0]);
if (m[0] === "") re.lastIndex++;
}
html += esc(text.slice(last));
out.innerHTML = html;
list.textContent = found.length
? "matches: " + found.join(", ")
: "no matches";
}
document.getElementById("go").addEventListener("click", render);
render();
tea|cola|milk|lemon(ade)?
tea
OR
cola
OR
milk
OR
lemon(ade)?
Each branch of the alternation is tried in turn at the current position; the first one that matches wins.
Alternation vs. square brackets
You have already met something that feels like OR: character classes. pattern:licen[sc]e matches match:license or match:licence, because [sc] says “an s or a c here”.
The difference is scope. Square brackets choose between single characters (or character classes). Alternation chooses between whole expressions — anything you can write in a pattern.
So these two are equivalent, since each alternative is a single character:
pattern:licen(s|c)e behaves exactly like pattern:licen[sc]e.
But watch what happens without the parentheses:
pattern:licens|ce matches match:licens or match:ce — not what you meant.
That second example is the trap that trips up almost everyone. | has very low precedence: it splits the pattern at the widest level it can reach.
Parentheses set the boundaries
Because | reaches as far as it can in both directions, you use parentheses to fence it in. Compare:
pattern:we ship cars|bikes matches match:we ship cars or match:bikes.
In the first pattern the OR sits between the entire left side (we ship cars) and the entire right side (bikes). The words “we ship” are stuck to the first alternative only.
we ship cars|bikes
we ship cars
OR
bikes
we ship (cars|bikes)
we ship
carsORbikes
Without parentheses, | divides the whole pattern. With them, it divides only what's inside.
Edit the sentence below and watch two patterns react to the same text in real time. The only difference between them is a pair of parentheses, yet they capture very different things.
const reA = /we ship cars|bikes/gi;
const reB = /we ship (cars|bikes)/gi;
const src = document.getElementById("src");
function esc(s) {
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function highlight(re, text) {
let html = "";
let last = 0;
let m;
const found = [];
re.lastIndex = 0;
while ((m = re.exec(text)) !== null) {
html += esc(text.slice(last, m.index));
html += "<mark>" + esc(m[0]) + "</mark>";
last = m.index + m[0].length;
found.push(m[0]);
if (m[0] === "") re.lastIndex++;
}
html += esc(text.slice(last));
return { html, found };
}
function render() {
const text = src.value;
const a = highlight(reA, text);
const b = highlight(reB, text);
document.getElementById("a").innerHTML = a.html;
document.getElementById("b").innerHTML = b.html;
document.getElementById("al").textContent = a.found.length ? "matches: " + a.found.join(" | ") : "no matches";
document.getElementById("bl").textContent = b.found.length ? "matches: " + b.found.join(" | ") : "no matches";
}
src.addEventListener("input", render);
render();
A worked example: matching a coordinate
Earlier articles asked for a pattern that finds a coordinate written as dd:mm (degrees and minutes), such as 45:30. The naive attempt is pattern:\d\d:\d\d. It works on well-formed input, but it is too loose — it happily accepts 91:99, which is not a real latitude.
To do better, we constrain each part to its valid range.
Degrees of latitude run from 00 to 90. Break that into cases by the first digit:
First digit 0 to 8: the second digit can be anything. That’s pattern:[0-8]\d.
First digit 9: the only valid value is 90, so the second digit must be 0. That’s pattern:90.
No other first digit is valid.
Join those two cases with alternation: pattern:[0-8]\d|90.
Minutes run from 00 to 59. The first digit is 0–5, the second is any digit: pattern:[0-5]\d.
degrees 00–90
[0-8]\d|90
:
minutes 00–59
[0-5]\d
Valid ranges for each digit position in dd:mm.
Now glue degrees and minutes together and you get: pattern:[0-8]\d|90:[0-5]\d. Which is broken. Here comes the precedence trap again.
Because | reaches as wide as it can, the engine reads the pattern as an OR between two big chunks:
[0-8]\d | 90:[0-5]\d
That means “either pattern:[0-8]\d by itself, or pattern:90:[0-5]\d”. The :[0-5]\d minutes part got glued onto the second alternative only, and the first alternative lost its minutes entirely.
[0-8]\d|90:[0-5]\d wrong
[0-8]\d
OR
90:[0-5]\d
no minutes on the left branch
The bug: | splits the whole pattern, so minutes attach to only one side.
The fix is to fence the OR so it covers only the degrees. Wrap the two degree cases in parentheses: pattern:([0-8]\d|90):[0-5]\d. Now the alternation stops at the closing ), and the :[0-5]\d applies to whichever degree branch matched.
([0-8]\d|90):[0-5]\d correct
[0-8]\dOR90
:[0-5]\d
The fix: parentheses limit | to the degrees, so minutes apply to both branches.
The final solution:
let regexp = /([0-8]\d|90):[0-5]\d/g;alert("45:30 00:00 90:00 91:99 7:5".match(regexp)); // 45:30,00:00,90:00
Notice what got filtered out. 91:99 fails because 91 is not a valid latitude degree and 99 is not a valid minute. 7:5 fails because each field needs two digits.
Flip between the broken pattern and the fixed one to feel the precedence trap for yourself. The broken version drops the minutes from the first degree branch, so it grabs stray digit pairs; the fixed version only accepts real coordinates.
interactiveBroken vs. fixed coordinate pattern
Live example — Broken vs. fixed coordinate pattern