# Timeline

> Vertical list of events with a rail on the inline-start side, an icon or avatar marker, title, description and relative time.

Source: https://docs.nasaqui.com/components/timeline

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/timeline.json
```

A vertical history of things that happened: an activity feed, an order's tracking, an audit trail. Each item has a marker (an icon or the actor's avatar) on a rail, a title, an optional description and a relative time.

## When to use

- A chronological list where the order and the "when" matter.
- Activity on a record (issue, invoice, order).

## When not to use

- Rows the user acts on (open, mark read): use [`NotificationItem`](https://docs.nasaqui.com/components/notification-item).
- A sequence the user moves through: use [`Stepper`](https://docs.nasaqui.com/components/stepper).
- Tabular history with many columns: use [`Table`](https://docs.nasaqui.com/components/table).

## Import

```tsx
import { Timeline, TimelineItem } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Timeline, TimelineItem } from "@fadymondy/nasaq/web";
import { GitPullRequest } from "lucide-react";

export function Activity() {
  return (
    <Timeline>
      <TimelineItem actor={{ name: "Sara Alharbi" }} title="Assigned MH-142 to Khaled" time={new Date(Date.now() - 300_000)} />
      <TimelineItem icon={<GitPullRequest />} title="Pull request #48 opened" description="Post-login redirect" time="2026-09-27T10:00:00Z" />
    </Timeline>
  );
}
```

## Anatomy

```
Timeline                      data-slot="timeline"            <ol>
└─ TimelineItem               data-slot="timeline-item"       <li>
   ├─ marker                  data-slot="timeline-marker"     `icon` tile, else <Avatar> from `actor`, else a dot
   ├─ rail                    data-slot="timeline-rail"       hidden on the last item
   └─ content                 data-slot="timeline-content"
      ├─ title + time         time is <DateTime relative>
      ├─ description
      └─ children
```

## API

### `Timeline`

`TimelineProps extends ComponentProps<"ol">`. No own props.

### `TimelineItem`

`TimelineItemProps extends Omit<ComponentProps<"li">, "title">`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `ReactNode` | required | What happened. |
| `description?` | `ReactNode` | none | Detail line. |
| `time?` | `Date \| number \| string` | none | When. Shown relative ("3 hours ago" / "قبل 3 ساعات"); the absolute date is the `title` tooltip and `<time dateTime>` carries ISO. |
| `icon?` | `ReactNode` | none | Marker glyph. Wins over `actor`. |
| `actor?` | `{ name: string; avatar?: string }` | none | Avatar marker. |
| `children?` | `ReactNode` | none | Extra content below the description. |

## Examples

### Arabic feed

```tsx
import { Timeline, TimelineItem } from "@fadymondy/nasaq/web";

export function Feed() {
  return (
    <Timeline>
      <TimelineItem actor={{ name: "سارة الحربي" }} title="أسندت MH-142 إلى خالد" time={new Date(Date.now() - 300_000)} />
      <TimelineItem title="أُنشئ المشروع" time="2026-08-01T09:00:00Z" />
    </Timeline>
  );
}
```

## Accessibility

- It is an ordered list; items are read in DOM order, so put the newest first (or oldest first) consistently.
- The rail and the plain dot are decorative (`aria-hidden`). An icon marker is decorative too, so the title must carry the meaning.
- Times are `<time dateTime>` elements.
- The list is not interactive; there is no keyboard behaviour.

## RTL & i18n

- The rail and markers sit on the inline start: the left in English, the right in Arabic. No physical classes are used.
- Relative times use the provider locale and Latin digits by default (`DateTime`).
- Identifiers in Arabic titles (issue keys) should be wrapped in `<bdi>` or `Ltr`.

## Styling & tokens

- Rail `bg-border`; icon tile `bg-secondary` with `border-border`; text `text-foreground` and `text-muted-foreground`.
- Target `[data-slot=timeline-item]`. Extend with `className`; never use raw hex.

## Do / Don't

- **Do** keep titles to one sentence and let the description carry detail.
- **Do** pass real dates, not pre-formatted strings.
- **Don't** put buttons in a marker or title; use `children` for actions.

## Related

- [NotificationItem](https://docs.nasaqui.com/components/notification-item) · [Avatar](https://docs.nasaqui.com/components/avatar) · [Stepper](https://docs.nasaqui.com/components/stepper)

## Lab

https://docs.nasaqui.com/?path=/docs/components-data-display-timeline--docs

## Code

### React

```tsx
import { Timeline, TimelineItem } from "@fadymondy/nasaq/web";
import { GitPullRequest } from "lucide-react";

export function Activity() {
  return (
    <Timeline>
      <TimelineItem actor={{ name: "Sara Alharbi" }} title="Assigned MH-142 to Khaled" time={new Date(Date.now() - 300_000)} />
      <TimelineItem icon={<GitPullRequest />} title="Pull request #48 opened" description="Post-login redirect" time="2026-09-27T10:00:00Z" />
    </Timeline>
  );
}
```

### shadcn

```tsx
import { Timeline, TimelineItem } from "@/components/ui/timeline";
import { GitPullRequest } from "lucide-react";

export function Activity() {
  return (
    <Timeline>
      <TimelineItem actor={{ name: "Sara Alharbi" }} title="Assigned MH-142 to Khaled" time={new Date(Date.now() - 300_000)} />
      <TimelineItem icon={<GitPullRequest />} title="Pull request #48 opened" description="Post-login redirect" time="2026-09-27T10:00:00Z" />
    </Timeline>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqTimeline, NqTimelineItem } from "@fadymondy/nasaq/vue";
import { GitPullRequest } from "lucide-vue-next";

const fiveMinutesAgo = new Date(Date.now() - 300_000);
</script>

<template>
  <NqTimeline>
    <NqTimelineItem :actor="{ name: 'Sara Alharbi' }" title="Assigned MH-142 to Khaled" :time="fiveMinutesAgo" />
    <NqTimelineItem title="Pull request #48 opened" description="Post-login redirect" time="2026-09-27T10:00:00Z">
      <template #icon><GitPullRequest /></template>
    </NqTimelineItem>
  </NqTimeline>
</template>
```

### Blade

```blade
<x-nq::timeline>
    <x-nq::timeline.item :actor="['name' => 'Sara Alharbi']" title="Assigned MH-142 to Khaled" :time="now()->subMinutes(5)" />
    <x-nq::timeline.item title="Pull request #48 opened" description="Post-login redirect" time="2026-09-27T10:00:00Z">
        <x-slot:icon><x-lucide-git-pull-request /></x-slot:icon>
    </x-nq::timeline.item>
</x-nq::timeline>

<div class="mt-6" x-data="{ events: [{ title: 'Order placed', time: '2026-09-27T09:00:00Z' }], add() { this.events = [{ title: 'Packed', description: 'Box 2 of 2', time: '2026-09-27T10:00:00Z', actor: { name: 'Khaled Nasser' } }].concat(this.events) } }">
    <x-nq::timeline id="live-timeline" items-expr="events">
        <x-nq::timeline.item title="Order placed" time="2026-09-27T09:00:00Z" />
    </x-nq::timeline>
    <x-nq::button id="live-add" size="sm" class="mt-3" x-on:click="add()">Add an event</x-nq::button>
</div>
```

### HTML + Alpine

```html
<ol data-slot="timeline" class="m-0 flex list-none flex-col p-0"><li data-slot="timeline-item" class="group/timeline grid grid-cols-[2rem_1fr] gap-x-3">
    <div class="flex flex-col items-center">
        <span data-slot="timeline-marker" class="flex size-8 shrink-0 items-center justify-center">
                            <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Sara Alharbi" >SA</span>
    </span>
                    </span>
        <span aria-hidden="true" data-slot="timeline-rail" class="my-1 w-px flex-1 bg-border group-last/timeline:hidden"></span>
    </div>
    <div data-slot="timeline-content" class="flex min-w-0 flex-col gap-1 pb-6 pt-1 group-last/timeline:pb-0">
        <div class="flex items-baseline justify-between gap-3">
            <p class="min-w-0 text-body-sm font-medium text-foreground">Assigned MH-142 to Khaled</p>
                            <time data-slot="date-time" datetime="2026-09-29T08:55:00+00:00" dir="auto"  title="Sep 29, 2026"     class="tabular-nums [unicode-bidi:isolate] shrink-0 text-caption text-muted-foreground">5 minutes ago</time>
                    </div>
                    </div>
</li>
    <li data-slot="timeline-item" class="group/timeline grid grid-cols-[2rem_1fr] gap-x-3">
    <div class="flex flex-col items-center">
        <span data-slot="timeline-marker" class="flex size-8 shrink-0 items-center justify-center">
                            <span class="flex size-8 items-center justify-center rounded-full border border-border bg-secondary text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="18" cy="18" r="3"/>
  <circle cx="6" cy="6" r="3"/>
  <path d="M13 6h3a2 2 0 0 1 2 2v7"/>
  <line x1="6" x2="6" y1="9" y2="21"/>
</svg></span>
                    </span>
        <span aria-hidden="true" data-slot="timeline-rail" class="my-1 w-px flex-1 bg-border group-last/timeline:hidden"></span>
    </div>
    <div data-slot="timeline-content" class="flex min-w-0 flex-col gap-1 pb-6 pt-1 group-last/timeline:pb-0">
        <div class="flex items-baseline justify-between gap-3">
            <p class="min-w-0 text-body-sm font-medium text-foreground">Pull request #48 opened</p>
                            <time data-slot="date-time" datetime="2026-09-27T10:00:00+00:00" dir="auto"  title="Sep 27, 2026"     class="tabular-nums [unicode-bidi:isolate] shrink-0 text-caption text-muted-foreground">1 day ago</time>
                    </div>
                    <p class="text-body-sm text-muted-foreground">Post-login redirect</p>
                    </div>
</li></ol>
<div class="mt-6" x-data="{ events: [{ title: 'Order placed', time: '2026-09-27T09:00:00Z' }], add() { this.events = [{ title: 'Packed', description: 'Box 2 of 2', time: '2026-09-27T10:00:00Z', actor: { name: 'Khaled Nasser' } }].concat(this.events) } }">
    <ol data-slot="timeline" x-data="nqTimeline(JSON.parse('{\u0022locale\u0022:\u0022en\u0022}'))" x-effect="sync(events)" id="live-timeline" class="m-0 flex list-none flex-col p-0"><li data-slot="timeline-item" class="group/timeline grid grid-cols-[2rem_1fr] gap-x-3">
    <div class="flex flex-col items-center">
        <span data-slot="timeline-marker" class="flex size-8 shrink-0 items-center justify-center">
                            <span aria-hidden="true" class="size-2.5 rounded-full border-2 border-nq-line bg-background"></span>
                    </span>
        <span aria-hidden="true" data-slot="timeline-rail" class="my-1 w-px flex-1 bg-border group-last/timeline:hidden"></span>
    </div>
    <div data-slot="timeline-content" class="flex min-w-0 flex-col gap-1 pb-6 pt-1 group-last/timeline:pb-0">
        <div class="flex items-baseline justify-between gap-3">
            <p class="min-w-0 text-body-sm font-medium text-foreground">Order placed</p>
                            <time data-slot="date-time" datetime="2026-09-27T09:00:00+00:00" dir="auto"  title="Sep 27, 2026"     class="tabular-nums [unicode-bidi:isolate] shrink-0 text-caption text-muted-foreground">2 days ago</time>
                    </div>
                    </div>
</li>        <template x-for="(it, i) in items" x-bind:key="i">
            <li data-slot="timeline-item" data-nq-dynamic class="group/timeline grid grid-cols-[2rem_1fr] gap-x-3">
                <div class="flex flex-col items-center">
                    <span data-slot="timeline-marker" class="flex size-8 shrink-0 items-center justify-center">
                        <template x-if="it.actor">
                            <span data-slot="avatar" x-data="nqAvatar(it.actor.avatar ? 400 : 0)" class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
                                <template x-if="it.actor.avatar"><img data-slot="avatar-image" x-bind:src="it.actor.avatar" x-bind:alt="it.actor.name" class="size-full object-cover" style="display: none" x-bind="image"></template>
                                <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback" x-text="initials(it.actor.name)"></span>
                            </span>
                        </template>
                        <template x-if="! it.actor"><span aria-hidden="true" class="size-2.5 rounded-full border-2 border-nq-line bg-background"></span></template>
                    </span>
                    <span aria-hidden="true" data-slot="timeline-rail" class="my-1 w-px flex-1 bg-border group-last/timeline:hidden"></span>
                </div>
                <div data-slot="timeline-content" class="flex min-w-0 flex-col gap-1 pb-6 pt-1 group-last/timeline:pb-0">
                    <div class="flex items-baseline justify-between gap-3">
                        <p class="min-w-0 text-body-sm font-medium text-foreground" x-text="it.title"></p>
                        <template x-if="it.time"><time data-slot="date-time" dir="auto" class="shrink-0 text-caption text-muted-foreground tabular-nums [unicode-bidi:isolate]" x-bind:datetime="iso(it.time)" x-bind:title="iso(it.time)" x-text="ago(it.time)"></time></template>
                    </div>
                    <template x-if="it.description"><p class="text-body-sm text-muted-foreground" x-text="it.description"></p></template>
                </div>
            </li>
        </template>
</ol>
    <button data-slot="button"
     type="button"                         id="live-add" x-on:click="add()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5 mt-3">
        Add an event</button>
</div>
```
