Building Timeline and Activity Feed Components with Tailwind CSS
AI generated
tw
Tailwind CSS · UI Pattern · Timeline
Timeline and Activity Feed Components
Building the vertical line, icon badges and responsive behavior with Tailwind

Whether it is an order history, an audit log, or a project's activity feed, chronologically ordered events are one of the most common recurring UI patterns in web applications. Visually there is more to them than it first appears, from the vertical connecting line between events, through icon badges per event type, to handling description text of wildly varying length without breaking the grid. This article shows how to structure a timeline component cleanly with plain Tailwind CSS and where a compact activity list diverges from a detailed timeline view.

14 min read Timeline Activity Feed

1. Timeline and activity feed: related but distinct patterns

Both components display a sequence of events along a time axis, but they differ in density and purpose. A timeline emphasizes the narrative flow, for instance an order's status from placement through delivery, and expects relatively few, content-rich events. An activity feed, by contrast, logs many small events in a short span, for example every comment or status change in a project tool, and needs to stay far more compact and quickly scannable than a classic timeline.

For the Tailwind styling, that implies two different starting points: a timeline works with generous whitespace, clearly visible milestone dots, and often accompanying body text or even images per event. An activity feed, on the other hand, reduces every entry to a single or double line core statement with a timestamp, so twenty or more entries remain skimmable at a glance. Both variants still share the same structural skeleton of a vertical line, dots and a content block, which is built out in the next section.

2. Building the vertical line with dots

The structural core element of any timeline is a continuous vertical line that strings together the individual event dots. With Tailwind this can be implemented without pseudo-elements using a simple flexbox layout per row: a fixed-width left column holds the dot and line, and a right column holds the content. The line itself is a narrow div with w-px bg-slate-200, stretched across the full height of the left column through absolute positioning, while the dot sits above it as a small rounded circle with rounded-full.

For the line to read as continuous rather than interrupted at every event, it needs to run across the entire height of its parent, including through the area where the next dot sits, simply getting visually covered by it. On the last entry in the list, the line should end before the dot begins, otherwise a visually confusing line fragment dangles below the final event. The simplest fix is a Tailwind equivalent of a :last-child selector, using the last:hidden group utility on a separate line element per row.


<ol class="relative">
  <li class="relative flex gap-4 pb-8 last:pb-0">
    <!-- Left column: dot + line -->
    <div class="relative flex w-8 flex-none flex-col items-center">
      <span class="z-10 flex h-8 w-8 items-center justify-center rounded-full
                   bg-sky-500 text-white ring-4 ring-white dark:ring-slate-900">
        <!-- Icon here -->
      </span>
      <span class="absolute top-8 bottom-0 w-px bg-slate-200
                   dark:bg-slate-700 last:hidden"></span>
    </div>

    <!-- Right column: content -->
    <div class="flex-1 pt-1">
      <p class="text-sm font-medium text-slate-900 dark:text-slate-100">
        Order was shipped
      </p>
      <time class="text-xs text-slate-400">2 hours ago</time>
    </div>
  </li>
</ol>

3. Icon badges per event type

The dot on the timeline's line is the ideal spot to visually encode the event type rather than describing it purely in text. A generously sized circle of roughly 32 pixels with a small icon in the middle and a type-specific background color is usually enough to identify the event type at a glance, for instance a checkmark for completed steps, a clock for pending ones, and a warning triangle for failed events. A white or colored ring around each dot, typically ring-4 ring-white, matters here since it visually separates the dot from the line underneath, which would otherwise blend seamlessly into it.

The badge color palette should stick to a fixed, small set, for example green for successful, yellow or amber for pending, red for failed, and a neutral blue or gray for informational events. This color coding must never be the only source of information: the icon itself and accompanying text need to carry the same information, so users with color vision deficiencies can still correctly identify the event type. With a large number of distinct event types, a central mapping table in the code that defines type, icon and color in one place is worth the effort, rather than repeating that mapping throughout the template.

4. Responsive behavior with long description text

Once an event carries more than a short status line, for instance a full comment in an activity feed, the layout can no longer rely on a fixed line height per entry. The content block to the right of the line needs a flexible height, while the left column with dot and line has to independently track the actual height of each entry, otherwise entries of varying length produce a visually messy line with uneven spacing between the dots.

On narrow screens, the available horizontal space for text also gets tight, especially when an avatar image or a timestamp shares the same row. Applying Tailwind's flex-wrap to an entry's header area lets name, action and timestamp wrap onto two lines automatically on narrow screens, instead of getting clipped by a hard overflow-hidden. Long free-form text should generally carry break-words, so a single long word, such as a URL inside a comment, cannot push the layout past its available width.

5. Compact activity list versus detailed timeline

A compact activity list reduces every entry to the bare minimum: a small dot with no dedicated icon, a single-line core statement, and a relative timestamp on the right edge. Vertical spacing between entries stays correspondingly tight, usually no more than py-2, so as many events as possible stay visible without scrolling. This variant suits contexts where users want a fast overview of many events, for example inside a sidebar or a dashboard widget.

A detailed timeline view, on the other hand, deliberately invests more space per event: larger icon badges, a more prominent event title, an optional description, and sometimes even accompanying images or file attachments. Vertical spacing between events is chosen more generously here, often pb-8 or more, so each event reads as a self-contained, clearly delimited block. This variant fits contexts where the history itself is the central piece of information, for instance an order history or a project's progress, something a user is meant to follow closely rather than just skim.

6. Timestamps: relative time versus an absolute date

Timestamps in timelines are usually shown as relative time, such as '3 hours ago' or 'yesterday', because that reads faster than an absolute date. It matters to still keep the absolute date accessible, typically through the title attribute of the <time> element or a hover tooltip, so users can look up the exact moment when needed without the compact relative display having to give way for it. Visually the timestamp gets a noticeably muted color compared to the actual event text, usually text-slate-400, since it is relevant but clearly secondary to the actual content.

For longer timelines spanning several days or weeks, a purely relative time quickly loses precision, since '5 days ago' is less concrete than an actual date. A hybrid approach works well here: events from the last 24 hours get a relative label, older events get a fixed date formatted as day and month. This switch should happen consistently in one central spot in the formatting logic, not through duplicated markup with conditional display scattered across the template.

7. Grouping by day with dedicated date headers

Once an activity feed spans multiple days, grouping by calendar day noticeably helps with scanning the list, similar to how chat applications group messages by day. A date header, such as 'Today', 'Yesterday', or a specific date, deliberately breaks the continuous vertical line at that point and gets its own, slightly offset formatting with sticky top-0, so it stays visible while scrolling through the group and the user always knows which day they are currently looking at.

The timeline's line itself reasonably pauses at every day header and starts fresh underneath, rather than visually running straight through the break, since a continuous line across multiple day groups would suggest a single connected sequence even though these are actually separate daily sections. The date header usually gets a subtly offset background, such as bg-slate-50, to clearly separate it from the actual event entries without becoming visually too dominant.

8. Status-based color coding across the whole timeline

Beyond the color of a single icon badge, it is worth coloring the line itself based on status for process-oriented timelines, such as an order history. Completed segments of the line between two already-reached milestones get a bold color like bg-emerald-500, while the segment between the last reached milestone and the next, still-pending one stays gray. That creates the impression of a progress bar built directly into the timeline at a glance, without needing a separate progress element.

It matters not to let the color transition end abruptly at some arbitrary pixel, but to switch the color consistently right at the position of the relevant dot, so the visual boundary between done and pending lines up exactly with the corresponding milestone icon. For a failed or aborted action in the middle of a process, the line should continue in a warning color like bg-red-400 from that point onward, making it immediately clear that the remainder of the flow deviates from the normal case.

9. Accessibility: semantics, not just visuals

A timeline should be marked up semantically as an ordered list with <ol>, since the chronological order of events carries meaning that an unordered list would not convey. Every event icon that is purely decorative next to an already present text label gets aria-hidden="true", so screen readers do not announce it redundantly, while icons that are the sole source of information for the event type require an aria-label with a textual description of the status.

The relative timestamp should always be wrapped in a semantic <time datetime="..."> element carrying the machine-readable ISO date, even when the visible label just reads '2 hours ago', so screen readers and other tools can interpret the exact moment correctly. For interactive timelines where individual events can be expanded, the focus state of the clickable area needs to be clearly visible, usually through a Tailwind focus ring, since otherwise the timeline becomes practically unusable for keyboard-only users.

Event type Icon color Typical use Line color after
Success bg-emerald-500 Completed step, confirmed action bg-emerald-500
Pending bg-slate-300 Milestone not yet reached bg-slate-200
Warning bg-amber-500 Action needs attention but did not fail bg-amber-400
Failed bg-red-500 Aborted or failed action bg-red-400
Information bg-sky-500 Purely informational event, no status change bg-slate-200

Mironsoft

Tailwind CSS architecture, design systems, and performance

Tailwind frontends that stay maintainable despite thousands of utility classes?

We review existing Tailwind projects for bloated class lists, inconsistent design tokens, and unused CSS remnants, then build a design system that scales cleanly instead of getting messier with every component.

Design System Review

Checking tokens, spacing scale, and component consistency for maintainability.

Performance Optimization

Systematically reducing CSS bundle size, purge configuration, and load times.

Component Architecture

Building reusable, well-structured components instead of sprawling class lists.

10. Summary

Timeline and Activity Feed Components with Tailwind: The Essentials at a Glance

Skeleton

Two-column layout of dot-plus-line on the left and a content block on the right, line ends before the last dot.

Icon badges

Color, icon and text carry the same information redundantly, never color alone as the status source.

Compact vs. detailed

Activity list minimizes spacing and content per entry, timeline deliberately invests more space per event.

Accessibility

Ordered list, aria-label for icon-only information, time element with a machine-readable date.

11. FAQ: Timeline and Activity Feed Components with Tailwind: The Essentials at a Glance

1Should the timeline line be built as its own HTML element or as a CSS pseudo-element?
A dedicated div element is usually easier to maintain with Tailwind than a pseudo-element, since state classes like last:hidden apply to it directly. Pseudo-elements are possible but make conditional states more cumbersome.
2How many event types should a timeline distinguish by color at most?
In practice four to five clearly distinguishable colors are enough, more types overwhelm quick recognition. With more categories, combining color with different icon shapes is worth considering.
3When should I use a compact activity list instead of a detailed timeline?
Whenever many events occur in a short span and users primarily need a fast overview, for example in a dashboard sidebar. For rare, high-significance events that need to be followed closely, the detailed variant fits better.
4How do you handle a very large number of events without slowing the page down?
Beyond a few hundred entries, virtualization or pagination pays off, since the DOM otherwise grows unnecessarily large. Tailwind itself barely affects performance here, the bottleneck is the number of rendered elements.
5Should the date header stay sticky while scrolling?
For longer feeds, yes, it noticeably helps orientation. For short timelines with only a few day groups the effect is mostly negligible and can be skipped to keep the implementation simpler.
6How do you make sure the line stays clean with entries of varying height?
The line has to stretch relative to the actual height of its parent element, not a fixed pixel height. With top-8 bottom-0 on an absolutely positioned element it adapts automatically.
7Does every event strictly need its own icon?
No, in compact activity lists a plain colored dot without an icon is often enough. Icons pay off mainly where different event types need to be told apart quickly, for instance with more than two or three distinct statuses.
8How does the responsive presentation differ on mobile devices?
On narrow screens, header text usually wraps instead of getting clipped, and the left icon column can be slightly shrunk. The basic layout of line and content stays the same across all screen sizes though.
9Can a timeline be shown horizontally instead of vertically?
Yes, that is common mainly for a few clearly delimited steps, such as a checkout progress bar. With more than four or five steps or variable text length, the vertical variant usually stays more readable.
10How do you handle events without a known time, such as planned future steps?
Instead of a timestamp, show a placeholder label like 'Pending' and keep the icon dot in the neutral, pending color. An empty or incorrect date should never be shown, to avoid confusion.