Summit Themes
Blog

How to build a responsive alternating timeline with Tailwind CSS

A timeline is one of those components that looks deceptively simple until you try to make it responsive. On mobile you want a clean vertical list; on desktop you want that satisfying zig-zag layout where items alternate left and right around a centered spine. Getting both from the same markup — without a wall of custom CSS — is where Tailwind really earns its keep.

This guide walks through building a fully responsive alternating timeline using only Tailwind utility classes. We will start with the mobile-first vertical layout, then layer on the desktop alternating behavior. The final result works in any HTML context: a plain HTML file, an Astro component, a React component, whatever you are shipping.

How the layout works

The alternating effect needs two things: a centered vertical spine that runs the full height of the timeline, and a way to push odd items to the left and even items to the right. There are two common approaches — CSS Grid and Flexbox. We will use the Flexbox approach because it is simpler to reason about and the markup stays flat.

The spine is drawn with a before: pseudo-element on the container. Each item is a flex row. On mobile, items line up left-to-right with the icon on the left and the card filling the remaining width. On desktop (md: and up), we flip the flex direction on every odd item with md:odd:flex-row-reverse, which automatically pushes those cards to the left while keeping their icon centered on the spine. No absolute positioning, no JavaScript, no hardcoded widths per item.

Building the container

Start with the outer wrapper. This element gets the vertical spine via a before: pseudo-element. On mobile the spine sits 20px from the left edge (aligned to the icon center). On desktop it moves to the horizontal center of the container.

<div class="relative space-y-8
  before:absolute before:inset-0
  before:ml-5 before:-translate-x-px
  md:before:ml-0 md:before:left-1/2 md:before:-translate-x-1/2
  before:h-full before:w-0.5
  before:bg-gradient-to-b before:from-transparent
  before:via-slate-200 before:to-transparent">

  <!-- timeline items go here -->

</div>

Breaking that down:

  • relative — establishes a positioning context for the before: pseudo-element.
  • before:absolute before:inset-0 — stretches the pseudo-element to fill the container.
  • before:ml-5 before:-translate-x-px — on mobile, nudges the line to align with the icon's center.
  • md:before:ml-0 md:before:left-1/2 md:before:-translate-x-1/2 — on desktop, centers the line.
  • before:w-0.5 — a 2px line; thin enough to read as a guide without dominating.
  • The gradient (before:from-transparent ... before:to-transparent) softens the line at the top and bottom so it does not look clipped.

Building a timeline item

Each item has two parts: an icon that sits on the spine, and a card that holds the content. On mobile they sit in a left-to-right flex row. On desktop, md:odd:flex-row-reverse flips every other item so the card alternates sides.

<div class="relative flex items-center justify-between
  md:justify-normal md:odd:flex-row-reverse group">

  <!-- Icon -->
  <div class="flex items-center justify-center
    w-10 h-10 rounded-full border-2 border-slate-200
    bg-white shrink-0 shadow-sm
    md:order-1
    md:group-odd:-translate-x-1/2
    md:group-even:translate-x-1/2">
    <!-- swap in any icon or step number -->
    <span class="text-sm font-semibold text-slate-600">1</span>
  </div>

  <!-- Card -->
  <div class="w-[calc(100%-4rem)] md:w-5/12 p-5
    rounded-xl bg-white border border-slate-100 shadow-sm">
    <h3 class="font-semibold text-slate-800">Step title</h3>
    <p class="mt-1 text-sm text-slate-500">
      A short description of what happened at this point.
    </p>
    <time class="block mt-2 text-xs text-slate-400">March 2025</time>
  </div>

</div>

The key classes to understand on the icon element:

  • md:order-1 — in CSS Flexbox, order: 1 pushes the icon after the card in the natural source order. Because flex-row-reverse is applied on odd items, "after" visually becomes "between" — the icon lands in the center gap between the two sides of the timeline.
  • md:group-odd:-translate-x-1/2 — nudges the icon left by half its width on odd rows, so it sits exactly on the spine rather than to one side of it.
  • md:group-even:translate-x-1/2 — same correction in the opposite direction for even rows.

The card's w-[calc(100%-4rem)] fills the available width on mobile (subtracting the icon width and gap). On desktop, md:w-5/12 constrains it to just under half the container, leaving room for the icon and the empty opposite side.

A complete two-item example

Here is the full markup for a three-item timeline so you can see the pattern repeat:

<!-- Timeline container -->
<div class="max-w-3xl mx-auto px-4">
  <div class="relative space-y-8
    before:absolute before:inset-0
    before:ml-5 before:-translate-x-px
    md:before:ml-0 md:before:left-1/2 md:before:-translate-x-1/2
    before:h-full before:w-0.5
    before:bg-gradient-to-b before:from-transparent
    before:via-slate-200 before:to-transparent">

    <!-- Item 1 — lands on the RIGHT on desktop -->
    <div class="relative flex items-center justify-between
      md:justify-normal md:odd:flex-row-reverse group">
      <div class="flex items-center justify-center w-10 h-10
        rounded-full border-2 border-violet-300 bg-white shrink-0 shadow-sm
        md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2">
        <span class="text-sm font-bold text-violet-600">1</span>
      </div>
      <div class="w-[calc(100%-4rem)] md:w-5/12 p-5
        rounded-xl bg-white border border-slate-100 shadow-sm">
        <h3 class="font-semibold text-slate-800">Kick-off</h3>
        <p class="mt-1 text-sm text-slate-500">
          Initial planning, stakeholder alignment, and scope definition.
        </p>
        <time class="block mt-2 text-xs text-slate-400">January 2025</time>
      </div>
    </div>

    <!-- Item 2 — lands on the LEFT on desktop -->
    <div class="relative flex items-center justify-between
      md:justify-normal md:odd:flex-row-reverse group">
      <div class="flex items-center justify-center w-10 h-10
        rounded-full border-2 border-violet-300 bg-white shrink-0 shadow-sm
        md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2">
        <span class="text-sm font-bold text-violet-600">2</span>
      </div>
      <div class="w-[calc(100%-4rem)] md:w-5/12 p-5
        rounded-xl bg-white border border-slate-100 shadow-sm">
        <h3 class="font-semibold text-slate-800">Design sprint</h3>
        <p class="mt-1 text-sm text-slate-500">
          Wireframes, component library, and design token decisions locked in.
        </p>
        <time class="block mt-2 text-xs text-slate-400">February 2025</time>
      </div>
    </div>

    <!-- Item 3 — lands on the RIGHT on desktop -->
    <div class="relative flex items-center justify-between
      md:justify-normal md:odd:flex-row-reverse group">
      <div class="flex items-center justify-center w-10 h-10
        rounded-full border-2 border-violet-300 bg-white shrink-0 shadow-sm
        md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2">
        <span class="text-sm font-bold text-violet-600">3</span>
      </div>
      <div class="w-[calc(100%-4rem)] md:w-5/12 p-5
        rounded-xl bg-white border border-slate-100 shadow-sm">
        <h3 class="font-semibold text-slate-800">Launch</h3>
        <p class="mt-1 text-sm text-slate-500">
          Deployed to production, monitoring live, post-launch review scheduled.
        </p>
        <time class="block mt-2 text-xs text-slate-400">March 2025</time>
      </div>
    </div>

  </div>
</div>

Customizing with Tailwind v4 @theme

If you are on Tailwind v4 and want the timeline colors to follow your brand tokens, define them once in your CSS file using the @theme directive. Any variable defined under @theme automatically generates utility classes — no JavaScript config file needed.

@import "tailwindcss";

@theme {
  --color-brand-500: oklch(0.55 0.22 265);
  --color-brand-200: oklch(0.82 0.10 265);
}

After that, border-brand-200, text-brand-500, and bg-brand-500 are all valid utility classes you can drop straight into the icon element.

Practical variations

Horizontal connector lines on desktop

Some designs show a short horizontal rule connecting the card to the icon. You can fake this with a before: pseudo-element on the card itself, toggled only at the md: breakpoint, pointing left or right depending on which side the card lands. It is more markup, but the visual result reads clearly for process-heavy content like project timelines or onboarding sequences.

Colored status dots instead of numbers

Replace the step number with a small filled circle and swap the border color based on status. A class like bg-emerald-400 for "completed" and bg-amber-400 for "in progress" is immediately scannable without any legend. If you are rendering from a data array (in Astro or React), map the status field to the color class at render time.

Collapse to single-column on medium screens

The md: breakpoint triggers the alternating layout at 768px by default. If your timeline lives inside a narrow sidebar or a card, you may want to keep the single-column layout at md and only go alternating at lg. Replace every md: prefix with lg: — that is the only change needed.

Accessibility notes

A few quick checks before you ship:

  • Wrap the timeline in a <ol> if the entries represent an ordered sequence, or a <ul> for unordered events. This tells screen readers there is a list, not just a pile of divs.
  • Use <time datetime="2025-03">March 2025</time> rather than a plain <span> for dates. The datetime attribute provides a machine-readable value.
  • The decorative spine (the before: pseudo-element) is invisible to assistive technology by design — pseudo-elements do not appear in the accessibility tree, so no aria-hidden is needed.

Wrapping up

The alternating timeline pattern looks like it should need floats, absolute positioning, or a media-query-heavy custom stylesheet. In practice, the combination of before: pseudo-elements for the spine, group and odd: variants for alternation, and a couple of translate corrections for the icon gets you there cleanly. The entire layout is driven by three or four mobile-first utility groups, and switching from a stacked to an alternating layout is one breakpoint prefix away. From here, swap the step numbers for icons, add entrance animations with motion-safe: variants, or feed the items from a content collection — the structure stays the same.