Open demo

CSS code

HTML
<ol class="flip-tl">
  <li class="node done"><span class="dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span>
    <h2>Order placed <span class="sr">(done)</span></h2><p><time datetime="2024-09-02T09:14">2 Sep, 09:14</time></p></li>
  <li class="node done"><span class="dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span>
    <h2>Packed <span class="sr">(done)</span></h2><p><time datetime="2024-09-02T16:40">2 Sep, 16:40</time></p></li>
  <li class="node now" aria-current="step"><span class="dot" aria-hidden="true"></span>
    <h2>In transit <span class="sr">(current step)</span></h2><p>Out for delivery</p></li>
  <li class="node todo"><span class="dot" aria-hidden="true"></span>
    <h2>Delivered <span class="sr">(not yet)</span></h2><p>Expected <time datetime="2024-09-04">4 Sep</time></p></li>
</ol>
CSS
/* One <ol>, two axes: a horizontal progress track that becomes a vertical list below 700px. */
.flip-tl{--ft-done:#16a34a;--ft-now:#4f46e5;--ft-line:#dde3f0;--ft-muted:#5f6a86;--ft-todo:#646f8b;
  display:flex;width:100%;max-width:860px;margin:0;padding:0;list-style:none}
.flip-tl,.flip-tl *{box-sizing:border-box}
.flip-tl :where(li,h2,p){margin:0;padding:0}
.flip-tl .node{flex:1;position:relative;padding-top:42px;text-align:center;padding-inline:8px}
.flip-tl .node::before{content:"";position:absolute;top:15px;left:0;right:0;height:3px;background:var(--ft-line)}
.flip-tl .node:first-child::before{left:50%}
.flip-tl .node:last-child::before{right:50%}
.flip-tl .node.done::before{background:var(--ft-done)}
.flip-tl .dot{position:absolute;top:0;left:50%;transform:translateX(-50%);width:32px;height:32px;
  border-radius:50%;display:grid;place-items:center;background:#fff;border:3px solid var(--ft-line);z-index:1}
.flip-tl .done .dot{background:var(--ft-done);border-color:var(--ft-done)}
.flip-tl .now .dot{border-color:var(--ft-now);box-shadow:0 0 0 5px rgba(79,70,229,.15)}
.flip-tl .dot svg{display:block;width:14px;height:14px;stroke:#fff;fill:none;stroke-width:3.4}
.flip-tl .now .dot::after{content:"";width:10px;height:10px;border-radius:50%;background:var(--ft-now)}
.flip-tl h2{font-size:.87rem;margin-bottom:3px}
.flip-tl p{font-size:.76rem;color:var(--ft-muted)}
.flip-tl .todo h2{color:var(--ft-todo)}
.flip-tl .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media(max-width:700px){
  .flip-tl{flex-direction:column;max-width:340px}
  .flip-tl .node{padding:0 0 26px 46px;text-align:left}
  .flip-tl .node:last-child{padding-bottom:0}
  .flip-tl .node::before{top:0;bottom:0;left:15px;right:auto;width:3px;height:auto}
  .flip-tl .node:first-child::before{top:16px;left:15px}
  .flip-tl .node:last-child::before{bottom:auto;height:16px}
  .flip-tl .dot{top:0;left:0;transform:none}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<ol class="flip-tl list-unstyled d-flex w-100 m-0">
  <li class="node done position-relative"><span class="dot d-grid rounded-circle" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span>
    <h2>Order placed <span class="visually-hidden">(done)</span></h2><p><time datetime="2024-09-02T09:14">2 Sep, 09:14</time></p></li>
  <li class="node done position-relative"><span class="dot d-grid rounded-circle" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span>
    <h2>Packed <span class="visually-hidden">(done)</span></h2><p><time datetime="2024-09-02T16:40">2 Sep, 16:40</time></p></li>
  <li class="node now position-relative" aria-current="step"><span class="dot d-grid rounded-circle" aria-hidden="true"></span>
    <h2>In transit <span class="visually-hidden">(current step)</span></h2><p>Out for delivery</p></li>
  <li class="node todo position-relative"><span class="dot d-grid rounded-circle" aria-hidden="true"></span>
    <h2>Delivered <span class="visually-hidden">(not yet)</span></h2><p>Expected <time datetime="2024-09-04">4 Sep</time></p></li>
</ol>
CSS
/* Bootstrap list + utilities; the axis flip at 700px is the one thing written as custom CSS. */
.flip-tl{--ft-done:#16a34a;--ft-now:#4f46e5;--ft-line:#dde3f0;--ft-muted:#5f6a86;--ft-todo:#646f8b;max-width:860px}
.flip-tl .node{flex:1;padding-top:42px;text-align:center;padding-inline:8px}
.flip-tl .node::before{content:"";position:absolute;top:15px;left:0;right:0;height:3px;background:var(--ft-line)}
.flip-tl .node:first-child::before{left:50%}
.flip-tl .node:last-child::before{right:50%}
.flip-tl .node.done::before{background:var(--ft-done)}
.flip-tl .dot{position:absolute;top:0;left:50%;transform:translateX(-50%);width:32px;height:32px;
  place-items:center;background:#fff;border:3px solid var(--ft-line);z-index:1}
.flip-tl .done .dot{background:var(--ft-done);border-color:var(--ft-done)}
.flip-tl .now .dot{border-color:var(--ft-now);box-shadow:0 0 0 5px rgba(79,70,229,.15)}
.flip-tl .dot svg{display:block;width:14px;height:14px;stroke:#fff;fill:none;stroke-width:3.4}
.flip-tl .now .dot::after{content:"";width:10px;height:10px;border-radius:50%;background:var(--ft-now)}
.flip-tl h2{font-size:.87rem;font-weight:700;line-height:normal;margin:0 0 3px}
.flip-tl p{font-size:.76rem;color:var(--ft-muted);margin:0}
.flip-tl .todo h2{color:var(--ft-todo)}
@media(max-width:700px){
  .flip-tl{flex-direction:column;max-width:340px}
  .flip-tl .node{padding:0 0 26px 46px;text-align:left}
  .flip-tl .node:last-child{padding-bottom:0}
  .flip-tl .node::before{top:0;bottom:0;left:15px;right:auto;width:3px;height:auto}
  .flip-tl .node:first-child::before{top:16px;left:15px}
  .flip-tl .node:last-child::before{bottom:auto;height:16px}
  .flip-tl .dot{top:0;left:0;transform:none}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<ol class="flex w-full max-w-[860px] max-[700px]:max-w-[340px] max-[700px]:flex-col">
  <li class="relative flex-1 px-2 pt-[42px] text-center before:absolute before:top-[15px] before:right-0 before:left-0 before:h-[3px] before:bg-ft-done first:before:left-1/2 last:before:right-1/2 max-[700px]:pt-0 max-[700px]:pr-0 max-[700px]:pb-[26px] max-[700px]:pl-[46px] max-[700px]:text-left max-[700px]:before:top-0 max-[700px]:before:bottom-0 max-[700px]:before:left-[15px] max-[700px]:before:right-auto max-[700px]:before:h-auto max-[700px]:before:w-[3px] max-[700px]:first:before:top-4 max-[700px]:first:before:left-[15px]">
    <span class="absolute top-0 left-1/2 z-[1] grid size-8 -translate-x-1/2 place-items-center rounded-full border-3 border-ft-done bg-ft-done max-[700px]:left-0 max-[700px]:translate-x-0" aria-hidden="true"><svg class="size-3.5 fill-none stroke-white stroke-[3.4]" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span>
    <h2 class="mb-[3px] text-[.87rem] font-bold">Order placed <span class="sr-only">(done)</span></h2><p class="text-[.76rem] text-ft-muted"><time datetime="2024-09-02T09:14">2 Sep, 09:14</time></p></li>
  <li class="relative flex-1 px-2 pt-[42px] text-center before:absolute before:top-[15px] before:right-0 before:left-0 before:h-[3px] before:bg-ft-done max-[700px]:pt-0 max-[700px]:pr-0 max-[700px]:pb-[26px] max-[700px]:pl-[46px] max-[700px]:text-left max-[700px]:before:top-0 max-[700px]:before:bottom-0 max-[700px]:before:left-[15px] max-[700px]:before:right-auto max-[700px]:before:h-auto max-[700px]:before:w-[3px]">
    <span class="absolute top-0 left-1/2 z-[1] grid size-8 -translate-x-1/2 place-items-center rounded-full border-3 border-ft-done bg-ft-done max-[700px]:left-0 max-[700px]:translate-x-0" aria-hidden="true"><svg class="size-3.5 fill-none stroke-white stroke-[3.4]" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span>
    <h2 class="mb-[3px] text-[.87rem] font-bold">Packed <span class="sr-only">(done)</span></h2><p class="text-[.76rem] text-ft-muted"><time datetime="2024-09-02T16:40">2 Sep, 16:40</time></p></li>
  <li class="relative flex-1 px-2 pt-[42px] text-center before:absolute before:top-[15px] before:right-0 before:left-0 before:h-[3px] before:bg-ft-line max-[700px]:pt-0 max-[700px]:pr-0 max-[700px]:pb-[26px] max-[700px]:pl-[46px] max-[700px]:text-left max-[700px]:before:top-0 max-[700px]:before:bottom-0 max-[700px]:before:left-[15px] max-[700px]:before:right-auto max-[700px]:before:h-auto max-[700px]:before:w-[3px]" aria-current="step">
    <span class="absolute top-0 left-1/2 z-[1] grid size-8 -translate-x-1/2 place-items-center rounded-full border-3 border-ft-now bg-white shadow-[0_0_0_5px_rgba(79,70,229,.15)] after:size-2.5 after:rounded-full after:bg-ft-now max-[700px]:left-0 max-[700px]:translate-x-0" aria-hidden="true"></span>
    <h2 class="mb-[3px] text-[.87rem] font-bold">In transit <span class="sr-only">(current step)</span></h2><p class="text-[.76rem] text-ft-muted">Out for delivery</p></li>
  <li class="relative flex-1 px-2 pt-[42px] text-center before:absolute before:top-[15px] before:right-0 before:left-0 before:h-[3px] before:bg-ft-line last:before:right-1/2 max-[700px]:pt-0 max-[700px]:pr-0 max-[700px]:pb-0 max-[700px]:pl-[46px] max-[700px]:text-left max-[700px]:before:top-0 max-[700px]:before:left-[15px] max-[700px]:before:right-auto max-[700px]:before:h-4 max-[700px]:before:w-[3px]">
    <span class="absolute top-0 left-1/2 z-[1] grid size-8 -translate-x-1/2 place-items-center rounded-full border-3 border-ft-line bg-white max-[700px]:left-0 max-[700px]:translate-x-0" aria-hidden="true"></span>
    <h2 class="mb-[3px] text-[.87rem] font-bold text-ft-todo">Delivered <span class="sr-only">(not yet)</span></h2><p class="text-[.76rem] text-ft-muted">Expected <time datetime="2024-09-04">4 Sep</time></p></li>
</ol>
CSS
@theme {
  --color-ft-done: #16a34a;
  --color-ft-now: #4f46e5;
  --color-ft-line: #dde3f0;
  --color-ft-muted: #5f6a86;
  --color-ft-todo: #646f8b;
}

About this set

A progress timeline that runs horizontally on wide screens and turns into a vertical list below 700 pixels, from one piece of markup. The demo is an order tracker with four steps: two completed steps with green filled circles and a tick, a current step with an indigo ring and an inner dot, and an upcoming step in a plain grey ring with a lighter heading. Use it for order status, onboarding progress or any short sequence that has to fit a desktop header and a phone screen. The steps are an ordered list. A pseudo-element on each step draws the connector, and at the breakpoint a single media query switches the list to a column and swaps the connector from a 3 pixel tall bar to a 3 pixel wide one, so nothing is duplicated or hidden. Each step carries its state as visually hidden text, done, current step or not yet, and the current one also has aria-current=”step”, so the progress is not conveyed by colour alone. Dates use time elements. The Bootstrap version keeps the axis flip in a small custom block, and the Tailwind version does it with max-[700px]: variants.

What’s included

  • One ordered list that is horizontal above 700px and vertical below
  • Done, current and upcoming states with a tick, a ring and a plain circle
  • State written in visually hidden text plus aria-current="step"
  • Connector drawn with pseudo-elements, no extra markup
  • Timestamps marked up with time elements

Accessibility

Steps are an ordered list, and each heading ends with visually hidden text giving its state, so a screen reader hears for example In transit (current step); the current item also carries aria-current="step". The circles, ticks and connector lines are aria-hidden or CSS decoration. Text colours meet 4.5:1, and nothing animates.

Customise it

Change --ft-done, --ft-now, --ft-line and --ft-todo on .flip-tl (plain CSS and Bootstrap) or the --color-ft-* theme colours (Tailwind). Edit the 700px media query, or the max-[700px]: prefix in Tailwind, to move the breakpoint.