Journey
The path sessions take, step by step. Pick a page in each step and a line links it to the next step.
Columns by default
Without data-orientation, steps sit side by side as
columns and scroll sideways when they overflow. Each step is an
<li> with a <header> and a
<ul> of pages. A page is a
<button aria-pressed>. Click it or press Enter
or Space to pick it.
-
Step 12,400 sessionsentered -
Step 2900 reached900 dropped off (50.0%) -
Step 3310 reached330 dropped off (51.6%)
<div class="journey" role="group" aria-label="Store journey">
<ol>
<li>
<header><span>Step 1</span><strong>2,400 sessions</strong><small>entered</small></header>
<ul>
<li><button type="button" aria-pressed="true" value="/"><span>Home</span><data value="1800">1,800</data></button></li>
<li><button type="button" aria-pressed="false" value="/sale"><span>Sale</span><data value="600">600</data></button></li>
</ul>
</li>
<li>
<header><span>Step 2</span><strong>900 reached</strong><small>900 dropped off (50.0%)</small></header>
<ul>
<li><button type="button" aria-pressed="false" value="/products/tshirt"><span>T-shirt</span><data value="640">640</data></button></li>
<li><button type="button" aria-pressed="true" value="/search"><span>Search</span><data value="260">260</data></button></li>
</ul>
</li>
<li>
<header><span>Step 3</span><strong>310 reached</strong><small>330 dropped off (51.6%)</small></header>
<ul>
<li><button type="button" aria-pressed="true" value="/cart"><span>Cart</span><data value="220">220</data></button></li>
<li><button type="button" aria-pressed="false" value="/products/tote"><span>Tote bag</span><data value="90">90</data></button></li>
</ul>
</li>
</ol>
</div>
Vertical
Steps stack downward, each indented a bit more. Each page takes a
full row, which suits longer data: name, path
(<small>), sessions and revenue
(<data>). The line drops from the picked page
and turns into the next step.
-
Step 12,400 sessionsentered -
Step 2620 reached1,180 dropped off (65.6%) -
Step 3140 reached480 dropped off (77.4%) -
Gift wrap/gift-wrap
-
Gift wrap/gift-wrap
<div class="journey" data-orientation="vertical" role="group" aria-label="Checkout journey">
<ol>
<li>
<header><span>Step 1</span><strong>2,400 sessions</strong><small>entered</small></header>
<ul>
<li><button type="button" aria-pressed="true" value="/spring-sale"><span>Summer Sale</span><small>/summer-sale</small><data value="1800">1,800 sessions</data><data value="9000">$9,000.00 · $5.00 RPV</data></button></li>
<li><button type="button" aria-pressed="false" value="/checkout"><span>Checkout</span><small>/checkout</small><data value="600">600 sessions</data><data value="4200">$4,200.00 · $7.00 RPV</data></button></li>
</ul>
</li>
<li>
<header><span>Step 2</span><strong>620 reached</strong><small>1,180 dropped off (65.6%)</small></header>
<ul>
<li><button type="button" aria-pressed="true" value="/checkout"><span>Checkout</span><small>/checkout</small><data value="620">620 sessions</data><data value="9300">$9,300.00 · $15.00 RPV</data></button></li>
</ul>
</li>
<li>
<header><span>Step 3</span><strong>140 reached</strong><small>480 dropped off (77.4%)</small></header>
<ul>
<li data-outcomes>
<span>Gift wrap</span><small>/gift-wrap</small>
<div>
<button type="button" aria-pressed="true" value="declined"><span>Declined</span><data value="96">96 sessions</data><data value="4800">$4,800.00 · $50.00 RPV</data></button>
<button type="button" aria-pressed="false" value="accepted"><span>Accepted</span><data value="44">44 sessions · 31.4% CVR</data><data value="2640">$2,640.00 · $60.00 RPV</data></button>
</div>
</li>
</ul>
</li>
</ol>
</div>
Pages with outcomes
For a page like an upsell, add data-outcomes to the
page's <li>. The name and path show once,
followed by a <div> with one button per outcome
(Declined / Accepted). Each outcome can be picked like a page, and
the line joins at the page's row. The Accepted side shows the take
rate as CVR.
-
Step 1530 sessionsentered -
Step 2412 reached118 dropped off (22.3%) -
Protection plan/upsell/protect
-
Protection plan/upsell/protect
-
Step 3108 reached3 dropped off (2.7%)
<div class="journey" role="group" aria-label="Upsell journey">
<ol>
<li>
<header><span>Step 1</span><strong>530 sessions</strong><small>entered</small></header>
<ul>
<li><button type="button" aria-pressed="true" value="/cart"><span>Cart</span><data value="530">530</data></button></li>
</ul>
</li>
<li>
<header><span>Step 2</span><strong>412 reached</strong><small>118 dropped off (22.3%)</small></header>
<ul>
<li data-outcomes>
<span>Protection plan</span><small>/upsell/protect</small>
<div>
<button type="button" aria-pressed="false" value="declined"><span>Declined</span><data value="301">301 sessions</data></button>
<button type="button" aria-pressed="true" value="accepted"><span>Accepted</span><data value="111">111 · 26.9% CVR</data></button>
</div>
</li>
</ul>
</li>
<li>
<header><span>Step 3</span><strong>108 reached</strong><small>3 dropped off (2.7%)</small></header>
<ul>
<li><button type="button" aria-pressed="true" value="/thank-you"><span>Thank you</span><data value="108">108</data></button></li>
</ul>
</li>
</ol>
</div>
Picked path only
data-view="picked" hides the other pages and
outcomes, leaving only the picked page in each step. The page sets
this attribute itself, for example from a checkbox or an "All
pages / Picked path" toggle.
-
Step 12,400 sessionsentered -
Step 2620 reached1,180 dropped off (65.6%)
<div class="stack" data-gap="sm">
<label class="field">
<input type="checkbox" id="jn-view" checked />
Picked path only
</label>
<div class="journey" data-orientation="vertical" data-view="picked" role="group" aria-label="Picked path" id="jn-picked">
<ol>
<li>
<header><span>Step 1</span><strong>2,400 sessions</strong><small>entered</small></header>
<ul>
<li><button type="button" aria-pressed="true"><span>Summer Sale</span><small>/summer-sale</small><data value="1800">1,800 sessions</data><data value="9000">$9,000.00 · $5.00 RPV</data></button></li>
<li><button type="button" aria-pressed="false"><span>Checkout</span><small>/checkout</small><data value="600">600 sessions</data><data value="4200">$4,200.00 · $7.00 RPV</data></button></li>
</ul>
</li>
<li>
<header><span>Step 2</span><strong>620 reached</strong><small>1,180 dropped off (65.6%)</small></header>
<ul>
<li><button type="button" aria-pressed="true"><span>Checkout</span><small>/checkout</small><data value="620">620 sessions</data><data value="9300">$9,300.00 · $15.00 RPV</data></button></li>
<li><button type="button" aria-pressed="false"><span>Cart</span><small>/cart</small><data value="410">410 sessions</data><data value="0">$0.00 · $0.00 RPV</data></button></li>
</ul>
</li>
</ol>
</div>
</div>
<script type="module">
const toggle = document.getElementById("jn-view");
toggle.addEventListener("change", () => {
const journey = document.getElementById("jn-picked");
if (toggle.checked) journey.dataset.view = "picked";
else delete journey.dataset.view;
});
</script>
Fill later steps on pick
Each time a page is picked, the button fires
tsunami:pick (bubbles) with
detail.step (step index from 0) and
detail.value (the button's value, or the
page name). The page listens for it and loads the next steps in
their place. Lines redraw on their own when steps or picks change.
-
Step 11,000 sessionsentered
Pick a page in step 1
<div class="journey" role="group" aria-label="Pick a page" id="jn-fill">
<ol>
<li>
<header><span>Step 1</span><strong>1,000 sessions</strong><small>entered</small></header>
<ul>
<li>
<button type="button" aria-pressed="false" value="home">
<span>Home</span><data value="700">700</data>
</button>
</li>
<li>
<button type="button" aria-pressed="false" value="blog">
<span>Blog</span><data value="300">300</data>
</button>
</li>
</ul>
</li>
</ol>
</div>
<p id="jn-fill-output">Pick a page in step 1</p>
<script type="module">
// Next pages for each picked value; a real page would fetch them.
const next = {
home: [
["product", "T-shirt", 420],
["sale", "Sale", 180],
],
blog: [
["product", "T-shirt", 90],
["home", "Home", 60],
],
product: [["cart", "Cart", 150]],
sale: [["cart", "Cart", 70]],
cart: [["checkout", "Checkout", 120]],
};
const journey = document.getElementById("jn-fill");
const output = document.getElementById("jn-fill-output");
journey.addEventListener("tsunami:pick", (event) => {
const { step, value } = event.detail;
output.textContent = `Picked: ${value} in step ${step + 1}`;
const steps = journey.querySelectorAll(":scope > ol > li");
for (const later of [...steps].slice(step + 1)) later.remove();
const pages = next[value];
if (!pages) return;
const total = pages.reduce((sum, page) => sum + page[2], 0);
const li = document.createElement("li");
li.innerHTML = `<header><span>Step ${step + 2}</span><strong>${total} reached</strong></header><ul></ul>`;
for (const [key, name, count] of pages) {
const item = document.createElement("li");
item.innerHTML = `<button type="button" aria-pressed="false"><span></span><data></data></button>`;
const button = item.firstElementChild;
button.value = key;
button.querySelector("span").textContent = name;
button.querySelector("data").value = count;
button.querySelector("data").textContent = count;
li.querySelector("ul").append(item);
}
journey.querySelector(":scope > ol").append(li);
});
</script>
Change the colour
The picked pages and the lines use
--journey-color (the accent by default), with
--journey-ink for the text on top. Set them on one
journey with a style attribute or a class. Use a variable such as
var(--ok-fg) so the colour follows light and dark.
-
Step 11,000 sessionsentered -
Step 2600 reached400 dropped off (40.0%)
<div class="journey" role="group" aria-label="Green journey" style="--journey-color: var(--ok-fg); --journey-ink: var(--bg)">
<ol>
<li>
<header><span>Step 1</span><strong>1,000 sessions</strong><small>entered</small></header>
<ul>
<li><button type="button" aria-pressed="true"><span>Home</span><data value="700">700</data></button></li>
<li><button type="button" aria-pressed="false"><span>Sale</span><data value="300">300</data></button></li>
</ul>
</li>
<li>
<header><span>Step 2</span><strong>600 reached</strong><small>400 dropped off (40.0%)</small></header>
<ul>
<li><button type="button" aria-pressed="true"><span>T-shirt</span><data value="420">420</data></button></li>
<li><button type="button" aria-pressed="false"><span>Search</span><data value="180">180</data></button></li>
</ul>
</li>
</ol>
</div>
API
journey.js works on every .journey on
the page, including ones added later. No init call needed. The
functions live on window.tsunami.journey.
| Function / event | Arguments / detail | Description |
|---|---|---|
pick(button) |
button: a page or outcome button in the
journey
|
Picks the page like a click: sets
aria-pressed="true", clears picks in the same
step and all later steps, fires
tsunami:pick and redraws the lines
|
draw(journey) |
journey: element .journey |
Redraws the lines now (they normally redraw when the size, steps or picks change) |
tsunami:pick |
{ step, value } |
Fired by the button after a pick, and bubbles.
step is the step index from 0.
value is the button's value, or
the text of its first <span> if none
|
| Key | Description |
|---|---|
| Tab | Move to the next page (native; every page is focusable) |
| Enter, Space | Pick the page (native button click) |
| ↓, ↑ | Move to the next or previous page in the step. In vertical mode it crosses into other steps |
| →, ← |
Move to the picked page in the next or previous step (or
the step's first page if none is picked). Reversed in
dir="rtl". Not used in vertical mode
|
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.journey |
Outer box. Scrolls sideways when the steps overflow. Add
role="group" and an
aria-label to name it for screen readers
|
|
data-orientation |
vertical; none = columns (default) |
Stacks steps top to bottom, each indented 1.5rem more (up to 40%). Pages take a full row |
data-view |
picked; none = all pages (default) |
Shows only the picked page or outcome in each step |
--journey-color |
colour; default var(--accent) |
Fill of picked pages and colour of the lines. Set on
.journey
|
--journey-ink |
colour; default var(--on-accent) |
Text colour on picked pages |
> ol > li |
One step | |
li > header |
span, strong, small
|
Step header: span step name (grey),
strong session count,
small drop-off count
|
li > ul > li |
One page in the step | |
button[aria-pressed] |
true, false |
A pickable page (or outcome). true = the
picked page in this step (dark border). JS sets it on
click. Add a value to send with
tsunami:pick
|
button > span |
Page name, truncated with … when too long | |
button > small |
Page path (grey monospace). With a small in column mode, the numbers go under the name | |
button > data |
value |
Numbers: the first is sessions (right of the name), the second is revenue · RPV (grey) |
li[data-outcomes] |
Present / absent |
A page with outcomes, such as an upsell:
span name, small path, then a
<div> of outcome buttons (Declined /
Accepted + CVR). Lines join at this row
|
> svg |
Lines linking the picked pages. JS creates and draws it
(aria-hidden). Do not write it yourself
|