James Carter
The address looks wrong. Emailed the customer to confirm it before shipping.
A vertical list of events for an order or a job, built with
<ol> and <time>.
Each event is an <li>: text followed by a
<time>. Put the newest at the top.
<ol class="timeline">
<li>
Payment captured
<time datetime="2026-10-29T18:41">Oct 29, 2026 18:41</time>
</li>
<li>
Order #1042 placed
<time datetime="2026-10-29T18:40">Oct 29, 2026 18:40</time>
</li>
</ol>
Put a <details> in the event. The
<summary> is the heading (it gets its own
arrow) and a <dl> holds the details. Add
open to start it open.
<ol class="timeline">
<li>
<details open>
<summary>Refund issued</summary>
<dl>
<dt>Amount</dt>
<dd>$52.00</dd>
<dt>Reason</dt>
<dd>Item arrived damaged</dd>
</dl>
</details>
<time datetime="2026-10-28T10:05">Oct 28, 2026 10:05</time>
</li>
<li>
Delivered
<time datetime="2026-10-26T16:36">Oct 26, 2026 16:36</time>
</li>
</ol>
data-variant="danger" turns the dot and text red. The
time stays grey. data-variant="ok" (green dot) works
the same way.
<ol class="timeline">
<li data-variant="danger">
<details open>
<summary>Subscription renewal payment failed</summary>
<dl>
<dt>Reason</dt>
<dd>Card declined</dd>
</dl>
</details>
<time datetime="2026-10-05T06:09">Oct 5, 2026 06:09</time>
</li>
<li data-variant="danger">
Uploading tracking information failed
<time datetime="2026-10-04T14:20">Oct 4, 2026 14:20</time>
</li>
<li>
Status changed from Active to Past due by the system
<time datetime="2026-10-04T06:09">Oct 4, 2026 06:09</time>
</li>
<li data-variant="ok">
Payment captured
<time datetime="2026-09-05T06:09">Sep 5, 2026 06:09</time>
</li>
</ol>
An .icon or .loader placed first in an
event replaces the dot. Add aria-busy="true" to the
step in progress and data-variant="pending" to steps
not started yet (grey text, hollow dot).
<ol class="timeline">
<li data-variant="ok">
<i class="icon icon-circle-check" aria-hidden="true"></i>
Creating your account
</li>
<li aria-busy="true">
<span class="loader" role="status" aria-label="In progress"></span>
Initializing your store
</li>
<li data-variant="pending">Importing products</li>
</ol>
The page script changes the attributes itself. The running step
gets a .loader, which turns into a check icon when
done, then the next step starts. Timeline has no JS of its own.
<div class="stack" data-gap="sm">
<ol class="timeline" id="tl-steps">
<li data-variant="pending">Creating your account</li>
<li data-variant="pending">Initializing your store</li>
<li data-variant="pending">Importing products</li>
</ol>
<div>
<button type="button" class="btn" data-size="sm" id="tl-steps-play">Play</button>
</div>
</div>
<script type="module">
const steps = [...document.querySelectorAll("#tl-steps > li")];
const play = document.getElementById("tl-steps-play");
const spinner = '<span class="loader" role="status" aria-label="In progress"></span>';
const check = '<i class="icon icon-circle-check" aria-hidden="true"></i>';
const start = (step) => {
step.removeAttribute("data-variant");
step.setAttribute("aria-busy", "true");
step.insertAdjacentHTML("afterbegin", spinner);
};
play.addEventListener("click", () => {
play.disabled = true;
for (const step of steps) {
step.querySelector(".icon, .loader")?.remove();
step.removeAttribute("aria-busy");
step.dataset.variant = "pending";
}
let current = 0;
start(steps[current]);
const timer = setInterval(() => {
const step = steps[current];
step.removeAttribute("aria-busy");
step.dataset.variant = "ok";
step.querySelector(".loader").outerHTML = check;
current += 1;
if (steps[current]) return start(steps[current]);
clearInterval(timer);
play.disabled = false;
}, 1500);
});
</script>
A staff note is an <article> in the event: an
<h4> author name, a <time>,
a <p> message and a
<footer> of buttons. Show the buttons only
while the note can still be edited.
The address looks wrong. Emailed the customer to confirm it before shipping.
<ol class="timeline">
<li>
Order placed on hold
<time datetime="2026-10-23T08:15">Oct 23, 2026 08:15</time>
</li>
<li>
<article>
<h4>James Carter</h4>
<time datetime="2026-10-23T08:10">Oct 23, 2026 08:10</time>
<p>The address looks wrong. Emailed the customer to confirm it before shipping.</p>
<footer>
<button type="button" class="btn" data-size="sm">Edit</button>
<button type="button" class="btn" data-variant="danger" data-size="sm">Delete</button>
</footer>
</article>
</li>
<li>
Order placed
<time datetime="2026-10-22T18:40">Oct 22, 2026 18:40</time>
</li>
</ol>
The first event is a <form> with a
<textarea> and a <footer> of
buttons plus a <small> hint. In the example,
the page script adds the new note below the form on Save. Cancel
is a type="reset" button.
<ol class="timeline" id="tl-notes">
<li>
<label for="tl-note">Add a note</label>
<form id="tl-note-form">
<textarea id="tl-note" name="note" placeholder="Add order note..." required></textarea>
<footer>
<button type="submit" class="btn" data-variant="primary" data-size="sm">Save</button>
<button type="reset" class="btn" data-size="sm">Cancel</button>
<small>Notes are editable for 5 minutes.</small>
</footer>
</form>
</li>
<li>
Confirmation email sent
<time datetime="2026-10-03T00:36">Oct 3, 2026 00:36</time>
</li>
<li>
Fulfillment status changed from Unfulfilled to On hold
<time datetime="2026-10-03T00:35">Oct 3, 2026 00:35</time>
</li>
</ol>
<script type="module">
const form = document.getElementById("tl-note-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const now = new Date();
const li = document.createElement("li");
const article = document.createElement("article");
const name = document.createElement("h4");
const time = document.createElement("time");
const text = document.createElement("p");
name.textContent = "You";
time.dateTime = now.toISOString();
time.textContent = now.toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
text.textContent = form.elements.note.value;
article.append(name, time, text);
li.append(article);
form.closest("li").after(li);
form.reset();
});
</script>
| Attribute / element | Values | Description |
|---|---|---|
ol.timeline |
List of events, newest at the top | |
> li |
One event, with a dot and a line to the next event (except the last) | |
li[data-variant] |
ok, danger,
pending; none = default (grey dot)
|
ok: green dot and leading icon.
danger: red dot and text.
pending: a step not started yet, with grey
text and a hollow dot
|
li[aria-busy] |
true |
Tells screen readers this step is in progress (pair with
.loader)
|
li > .icon:first-child,
li > .loader:first-child
|
An icon or loader placed first replaces the dot | |
time |
datetime |
Time of the event, on its own line in small grey text |
details > summary |
open |
An event that opens to show details. The summary gets its own arrow |
dl > dt + dd |
Name/value details in two columns inside a frame | |
article |
A note: h4 author and time on
one line, followed by p and
footer
|
|
footer |
Button row of a note or form. A small
inside it is a grey hint
|
|
form, textarea |
Add-note form in an event; the textarea is full width |