Skip to content
Brok UI

Loading…

No results

Timeline

Open source

Sequenced event displays — a free-form timeline and a stepper that covers both a numbered install/checkout sequence and a plain status stepper.

Version
v1.3.2
Stability
stable
License
MIT
Related
Stepper

Timeline — A vertical or horizontal timeline with alternating layout, variant nodes, optional cards, and an `aside` slot across the rail for a sticky date or year.

Preview

  1. Jan 2026

    Project kickoff

    Scope agreed and the team assembled.

  2. Feb 2026

    Prototype approval

    Stakeholders signed off on the prototype.

  3. Mar 2026

    Unexpected delays

    A third-party API outage pushed the timeline.

  4. Apr 2026

    Beta launch

    Shipped to the first cohort of users.

Orientation
previews.components.timeline.default.blade.php Blade
<x-ui.timeline class="mx-auto w-full max-w-2xl py-2">
    <x-ui.timeline.item>
        <x-ui.timeline.date>{{ __('Jan 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Project kickoff') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Scope agreed and the team assembled.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="secondary">
        <x-ui.timeline.date>{{ __('Feb 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Prototype approval') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Stakeholders signed off on the prototype.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="destructive">
        <x-ui.timeline.date>{{ __('Mar 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Unexpected delays') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('A third-party API outage pushed the timeline.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="outline" hollow>
        <x-ui.timeline.date>{{ __('Apr 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Beta launch') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Shipped to the first cohort of users.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
</x-ui.timeline>
Horizontal Current
Vertical Current

Installation

terminal
php artisan ui:add timeline

Registry contract

php artisan ui:add timeline writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/timeline.blade.php
  • blade resources/views/components/ui/timeline/item.blade.php
  • blade resources/views/components/ui/timeline/date.blade.php
  • blade resources/views/components/ui/timeline/title.blade.php
  • blade resources/views/components/ui/timeline/description.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

timeline.md
# Brok UI: Timeline (`timeline`)

A vertical or horizontal timeline with alternating layout, variant nodes, optional cards, and an `aside` slot across the rail for a sticky date or year.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add timeline
```

## Usage

```blade
<x-ui.timeline class="mx-auto w-full max-w-2xl py-2">
    <x-ui.timeline.item>
        <x-ui.timeline.date>{{ __('Jan 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Project kickoff') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Scope agreed and the team assembled.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="secondary">
        <x-ui.timeline.date>{{ __('Feb 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Prototype approval') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Stakeholders signed off on the prototype.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="destructive">
        <x-ui.timeline.date>{{ __('Mar 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Unexpected delays') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('A third-party API outage pushed the timeline.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="outline" hollow>
        <x-ui.timeline.date>{{ __('Apr 2026') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Beta launch') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Shipped to the first cohort of users.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
</x-ui.timeline>
```

## Props

- `orientation` (string, default `vertical`) — Declared by @props in the shipped Blade source.
- `alternating` (bool, default `true`) — Declared by @props in the shipped Blade source.
- `alignment` (string, default `top/left`) — Declared by @props in the shipped Blade source.
- `noCards` (bool, default `false`) — Declared by @props in the shipped Blade source.
- `horizItemWidth` (int, default `220`) — Declared by @props in the shipped Blade source.
- `vertItemMaxWidth` (int, default `350`) — Declared by @props in the shipped Blade source.
- `horizItemSpacing` (int, default `24`) — Declared by @props in the shipped Blade source.
- `vertItemSpacing` (int, default `32`) — Declared by @props in the shipped Blade source.
- `variant` (string, default `default`) — Declared by @props in the registry Blade source.
- `hollow` (bool, default `false`) — Declared by @props in the registry Blade source.

## Use when

- Use to summarize, sequence, or present data so users can scan it quickly.
- Presenting a sequence of dated or ordered events along a vertical or horizontal rail.
- Alternating events left and right on a vertical timeline for a narrative, or laying them out horizontally for a compact roadmap.

## Avoid when

- Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
- The events need sorting, filtering, or pagination as data; use table or list-panel instead.

## Anti-patterns

- Adding display ornament without informational value

## Rules

- Use the `<brok:timeline>` tag (or `<x-ui.timeline>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/timeline
- Registry JSON (files, props, contract): https://brokui.dev/r/open/timeline.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

orientations.blade.php Blade
{{-- Horizontal, every card above the rail. The rail sits at the same height
     in every item whatever the cards' heights, and each branch runs to the
     next dot. --}}
<x-ui.timeline orientation="horizontal" :alternating="false" alignment="top/left" class="max-w-full overflow-x-auto py-2">
    <x-ui.timeline.item>
        <x-ui.timeline.date>{{ __('09:00') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Standup') }}</x-ui.timeline.title>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="secondary">
        <x-ui.timeline.date>{{ __('11:30') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Design review') }}</x-ui.timeline.title>
        <x-ui.timeline.description>{{ __('Walk through the new onboarding flow.') }}</x-ui.timeline.description>
    </x-ui.timeline.item>
    <x-ui.timeline.item variant="outline">
        <x-ui.timeline.date>{{ __('15:00') }}</x-ui.timeline.date>
        <x-ui.timeline.title>{{ __('Deploy') }}</x-ui.timeline.title>
    </x-ui.timeline.item>
</x-ui.timeline>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
orientation string vertical Declared by @props in the shipped Blade source.
alternating bool true Declared by @props in the shipped Blade source.
alignment string top/left Declared by @props in the shipped Blade source.
noCards bool false Declared by @props in the shipped Blade source.
horizItemWidth int 220 Declared by @props in the shipped Blade source.
vertItemMaxWidth int 350 Declared by @props in the shipped Blade source.
horizItemSpacing int 24 Declared by @props in the shipped Blade source.
vertItemSpacing int 32 Declared by @props in the shipped Blade source.
variant string default Declared by @props in the registry Blade source.
hollow bool false Declared by @props in the registry Blade source.

Slots

  • default —
  • aside —
  • x-ui.timeline.item — Installed subcomponent from the registry item.
  • x-ui.timeline.date — Installed subcomponent from the registry item.
  • x-ui.timeline.title — Installed subcomponent from the registry item.
  • x-ui.timeline.description — Installed subcomponent from the registry item.

Data slots

Stable hooks for CSS overrides and browser tests.

timeline timeline-aside timeline-branch timeline-content timeline-item timeline-item-date timeline-item-description timeline-item-title timeline-rail

Behavior

  • orientation, alternating, and spacing props set on the root are read by every child timeline.item via @aware, so layout is configured once.
  • Item spacing is drawn as padding on each item rather than a flex gap, because a gap would leave a visible hole in the connecting rail.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Scannable data display

Present data for rapid scanning.

Use when

  • Use to summarize, sequence, or present data so users can scan it quickly.
  • Presenting a sequence of dated or ordered events along a vertical or horizontal rail.
  • Alternating events left and right on a vertical timeline for a narrative, or laying them out horizontally for a compact roadmap.

Avoid when

  • Do not add display-only ornament when the user needs actionable structure or exact comparison instead.
  • The events need sorting, filtering, or pagination as data; use table or list-panel instead.

Use instead

  • Table for exact comparison
  • Plain text for a single value

Anti-patterns

  • Adding display ornament without informational value
Anatomy
root timeline-item-date timeline-item-description timeline-item timeline-content timeline-rail timeline-branch timeline-aside timeline-item-title
Theming hooks
timeline timeline-aside timeline-branch timeline-content timeline-item timeline-item-date timeline-item-description timeline-item-title timeline-rail

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
No component-owned keyboard interaction; native element behavior applies.
Focus
none
  • The root is a semantic ol/li list, and the connecting rail between dots is purely decorative and marked aria-hidden.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

Livewire can update this component through forwarded wire:* attributes.

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/timeline.blade.php Blade
@props([
    'orientation' => 'vertical',
    'alternating' => true,
    'alignment' => 'top/left',
    'noCards' => false,
    'horizItemWidth' => 220,
    'vertItemMaxWidth' => 350,
    'horizItemSpacing' => 24,
    'vertItemSpacing' => 32,
])

@php
    // Layout props are inherited by <x-ui.timeline.item> via @aware, so authors
    // configure the whole timeline once on the root. `alternating` side is
    // resolved with CSS :nth-child inside the item (no per-item index needed).
    $vertical = $orientation !== 'horizontal';
    $gap = $vertical ? (int) $vertItemSpacing : (int) $horizItemSpacing;
    $flow = $vertical ? 'flex-col' : 'flex-row items-stretch';
@endphp

{{-- Spacing lives on the items (padding, read through @aware), never as a flex
     gap: each item draws its own stretch of the rail, so a gap between items
     would leave a hole in the line. --}}
<ol
    data-slot="timeline"
    data-orientation="{{ $orientation }}"
    {{ $attributes->merge(['class' => 'group/timeline flex '.$flow]) }}
>
    {{ $slot }}
</ol>
resources/views/components/ui/timeline/item.blade.php Blade
@aware([
    'orientation' => 'vertical',
    'alternating' => true,
    'alignment' => 'top/left',
    'noCards' => false,
    'horizItemWidth' => 220,
    'vertItemMaxWidth' => 350,
    'horizItemSpacing' => 24,
    'vertItemSpacing' => 32,
])

@props([
    'variant' => 'default',
    'hollow' => false,
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant);
    $vertical = $orientation !== 'horizontal';
    $cards = ! $noCards;
    $primarySide = in_array($alignment, ['top/left', 'top', 'left'], true);

    // Dot + branch colour per variant. `outline` is inherently hollow.
    // The dot carries the variant; the rail is one neutral hairline from the
    // first item to the last, so the timeline reads as one connected line.
    $accent = [
        'default' => ['bg' => 'bg-primary', 'border' => 'border-primary'],
        'secondary' => ['bg' => 'bg-muted-foreground', 'border' => 'border-muted-foreground'],
        'success' => ['bg' => 'bg-success', 'border' => 'border-success'],
        'destructive' => ['bg' => 'bg-destructive', 'border' => 'border-destructive'],
        'outline' => ['bg' => 'bg-background', 'border' => 'border-input'],
    ][$variant] ?? null;
    $accent ??= ['bg' => 'bg-primary', 'border' => 'border-primary'];
    $accent['line'] = 'bg-border';

    $isHollow = $hollow || $variant === 'outline';
    $dotClasses = $isHollow
        ? 'border-2 bg-background '.$accent['border']
        : $accent['bg'].' border-2 '.$accent['border'];

    $cardBox = $cards
        ? 'rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm'
        : '';

    // ── Flex direction / alternation ─────────────────────────────────────────
    if ($vertical) {
        $li = 'relative flex gap-4';
        $flip = $alternating ? 'even:flex-row-reverse' : ($primarySide ? 'flex-row-reverse' : '');
    } else {
        // Horizontal items are a three-row grid: content above, the rail, content
        // below. The rows are `minmax(0, 1fr)`, so once the flex row stretches
        // every item to the tallest one, the rail sits at the same height in each
        // item and the dots line up whatever the cards' heights are. When every
        // card sits on one side the empty row collapses and the rail stays at the
        // edge, so alternating items flip which row holds the card.
        $li = 'relative grid';
        $flip = $alternating
            ? 'grid-rows-[minmax(0,1fr)_auto_minmax(0,1fr)] even:[&>[data-slot=timeline-content]]:row-start-3 even:[&>[data-slot=timeline-content]]:self-start even:[&>[data-slot=timeline-content]]:pt-4 even:[&>[data-slot=timeline-content]]:pb-0'
            : ($primarySide ? 'grid-rows-[minmax(0,1fr)_auto]' : 'grid-rows-[auto_minmax(0,1fr)]');
    }

    // Card-content alignment (point text toward the branch).
    if ($vertical) {
        // The card hugs the rail on either side; the text inside always reads
        // from the start edge.
        $contentAlign = $alternating
            ? 'items-end text-start even:items-start'
            : ($primarySide ? 'items-start text-start' : 'items-end text-start');
        $contentWrap = 'flex min-w-0 flex-1 flex-col '.$contentAlign;
        // A zero/null max width lets the content take the whole column (a
        // changelog entry, a CV role) instead of the default 350px card.
        $cardStyle = (int) $vertItemMaxWidth > 0 ? 'max-width: '.((int) $vertItemMaxWidth).'px;' : '';
    } else {
        // Odd items sit above the rail (card pinned to its bottom edge), even
        // items below it (pinned to its top edge) when alternating.
        $contentWrap = 'flex w-full flex-col items-center text-center '
            .($alternating || $primarySide ? 'row-start-1 self-end pb-4' : 'row-start-2 self-start pt-4');
        $cardStyle = '';
    }

    // Hide the trailing branch on the last item.
    // The last item draws no branch below/after its dot.
    $hideLast = '[&:last-child_[data-slot=timeline-branch]]:hidden';

    // Item spacing is padding after the item (none after the last), so the
    // branch line runs through it unbroken.
    $liStyle = ($vertical ? '' : 'width: '.((int) $horizItemWidth).'px;')
        .($vertical ? 'padding-bottom: '.((int) $vertItemSpacing).'px;' : 'padding-inline-end: '.((int) $horizItemSpacing).'px;');
@endphp

<li
    data-slot="timeline-item"
    data-variant="{{ $variant }}"
    style="{{ $liStyle }}"
    {{ $attributes->merge(['class' => trim($li.' '.$flip.' '.$hideLast.' last:!pb-0 last:!pe-0')]) }}
>
    {{-- Content side --}}
    <div data-slot="timeline-content" class="{{ $contentWrap }}">
        <div @class([$cardBox, 'space-y-1', 'w-full' => ! $vertical || $cardStyle === '']) style="{{ $cardStyle }}">
            {{ $slot }}
        </div>
    </div>

    {{-- Rail: continuous branch line + node --}}
    @if ($vertical)
        <div class="relative flex w-3 shrink-0 items-start justify-center self-stretch" data-slot="timeline-rail">
            {{-- Runs from this dot through the item's own spacing to the next dot. --}}
            <span data-slot="timeline-branch" class="absolute top-2 w-px {{ $accent['line'] }}" style="inset-inline-start: calc(50% - 0.5px); bottom: -{{ (int) $vertItemSpacing }}px" aria-hidden="true"></span>
            <span class="relative z-10 mt-2 size-3 shrink-0 rounded-full ring-4 ring-background {{ $dotClasses }}"></span>
        </div>
    @else
        <div class="relative flex h-3 w-full items-center justify-center {{ $alternating ? 'row-start-2' : ($primarySide ? 'row-start-2' : 'row-start-1') }}" data-slot="timeline-rail">
            <span data-slot="timeline-branch" class="absolute top-1/2 h-px -translate-y-1/2 {{ $accent['line'] }}" style="inset-inline-start: 50%; inset-inline-end: calc(-50% - {{ (int) $horizItemSpacing }}px)" aria-hidden="true"></span>
            <span class="relative z-10 size-3 shrink-0 rounded-full ring-4 ring-background {{ $dotClasses }}"></span>
        </div>
    @endif

    {{-- The `aside` slot sits across the rail from the content (a sticky
         date, a year, a duration); it replaces the alternating spacer. --}}
    @if ($vertical && isset($aside))
        <div data-slot="timeline-aside" {{ $aside->attributes->merge(['class' => 'shrink-0'.($alternating ? ' flex-1' : '')]) }}>
            {{ $aside }}
        </div>
    @elseif ($alternating && $vertical)
        {{-- Spacer for alternating vertical layouts (the grid handles horizontal) --}}
        <div class="flex-1" aria-hidden="true"></div>
    @endif
</li>
resources/views/components/ui/timeline/date.blade.php Blade
<span
    data-slot="timeline-item-date"
    {{ $attributes->merge(['class' => 'block text-xs font-medium text-muted-foreground']) }}
>
    {{ $slot }}
</span>
resources/views/components/ui/timeline/title.blade.php Blade
<h3
    data-slot="timeline-item-title"
    {{ $attributes->merge(['class' => 'text-sm font-semibold text-foreground']) }}
>
    {{ $slot }}
</h3>
resources/views/components/ui/timeline/description.blade.php Blade
<p
    data-slot="timeline-item-description"
    {{ $attributes->merge(['class' => 'text-sm text-muted-foreground']) }}
>
    {{ $slot }}
</p>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.3.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0