Skip to content

Event Timeline

A vertical event log with connected status dots, titles and metadata.

A vertical event log with connected status dots, titles and metadata. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-timeline
import {
  Timeline,
  TimelineContent,
  TimelineDot,
  TimelineItem,
  TimelineMeta,
  TimelineTitle,
} from "@/registry/bases/newt/ui/timeline"

export default function TimelineDemo() {
  return (
    <Timeline className="w-full max-w-sm">
      <TimelineItem>
        <TimelineDot variant="brand">↻</TimelineDot>
        <TimelineContent>
          <TimelineTitle>Event title</TimelineTitle>
          <TimelineMeta>Detail · timestamp</TimelineMeta>
        </TimelineContent>
      </TimelineItem>
      <TimelineItem>
        <TimelineDot variant="success">✓</TimelineDot>
        <TimelineContent>
          <TimelineTitle>Event title</TimelineTitle>
          <TimelineMeta>Detail · timestamp</TimelineMeta>
        </TimelineContent>
      </TimelineItem>
    </Timeline>
  )
}
<script setup lang="ts">
import {
  Timeline,
  TimelineContent,
  TimelineDot,
  TimelineItem,
  TimelineMeta,
  TimelineTitle,
} from "~~/registry/bases/newt/ui/timeline"
</script>

<template>
  <Timeline class="w-full max-w-sm">
    <TimelineItem>
      <TimelineDot variant="brand">↻</TimelineDot>
      <TimelineContent>
        <TimelineTitle>Event title</TimelineTitle>
        <TimelineMeta>Detail · timestamp</TimelineMeta>
      </TimelineContent>
    </TimelineItem>
    <TimelineItem>
      <TimelineDot variant="success">✓</TimelineDot>
      <TimelineContent>
        <TimelineTitle>Event title</TimelineTitle>
        <TimelineMeta>Detail · timestamp</TimelineMeta>
      </TimelineContent>
    </TimelineItem>
  </Timeline>
</template>

Installation

pnpm dlx newtui@latest add timeline
npx newtui@latest add timeline
yarn dlx newtui@latest add timeline
bunx newtui@latest add timeline

Install the dependencies

pnpm dlx --yes class-variance-authority
npx --yes class-variance-authority
yarn dlx --yes class-variance-authority
bunx --yes class-variance-authority

Copy the source into your project

import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

import { cn } from "@/lib/utils"

const Timeline = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("flex flex-col", className)} {...props} />
))
Timeline.displayName = "Timeline"

const TimelineItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "relative flex gap-3 pb-5 last:pb-0",
      "before:absolute before:bottom-0 before:start-[11px] before:top-6 before:w-0.5 before:bg-newt-border before:content-[''] last:before:hidden",
      className
    )}
    {...props}
  />
))
TimelineItem.displayName = "TimelineItem"

const timelineDotVariants = cva(
  "z-[1] flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 border-newt-border bg-newt-bg-surface text-xs",
  {
    variants: {
      variant: {
        default: "",
        success: "border-newt-online text-newt-online",
        danger: "border-newt-dnd text-newt-dnd",
        brand: "border-newt-brand text-newt-mention-text",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface TimelineDotProps
  extends
    React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof timelineDotVariants> {}

const TimelineDot = React.forwardRef<HTMLDivElement, TimelineDotProps>(
  ({ className, variant, ...props }, ref) => (
    <div
      ref={ref}
      aria-hidden="true"
      className={cn(timelineDotVariants({ variant }), className)}
      {...props}
    />
  )
)
TimelineDot.displayName = "TimelineDot"

const TimelineContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("flex-1 pt-px", className)} {...props} />
))
TimelineContent.displayName = "TimelineContent"

const TimelineTitle = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-sm font-semibold text-newt-text-primary", className)}
    {...props}
  />
))
TimelineTitle.displayName = "TimelineTitle"

const TimelineMeta = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("mt-0.5 text-xs text-newt-text-muted", className)}
    {...props}
  />
))
TimelineMeta.displayName = "TimelineMeta"

export {
  Timeline,
  TimelineItem,
  TimelineDot,
  TimelineContent,
  TimelineTitle,
  TimelineMeta,
  timelineDotVariants,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('flex flex-col', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div
    :class="
      cn(
        'relative flex gap-3 pb-5 last:pb-0',
        'before:absolute before:bottom-0 before:start-[11px] before:top-6 before:w-0.5 before:bg-newt-border before:content-[\'\'] last:before:hidden',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

import { type TimelineDotVariants, timelineDotVariants } from "."

const props = defineProps<{
  variant?: TimelineDotVariants["variant"]
  class?: HTMLAttributes["class"]
}>()
</script>

<template>
  <div
    aria-hidden="true"
    :class="cn(timelineDotVariants({ variant: props.variant }), props.class)"
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('flex-1 pt-px', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('text-sm font-semibold text-newt-text-primary', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('mt-0.5 text-xs text-newt-text-muted', props.class)">
    <slot />
  </div>
</template>
import { cva, type VariantProps } from "class-variance-authority"

export { default as Timeline } from "./Timeline.vue"
export { default as TimelineItem } from "./TimelineItem.vue"
export { default as TimelineDot } from "./TimelineDot.vue"
export { default as TimelineContent } from "./TimelineContent.vue"
export { default as TimelineTitle } from "./TimelineTitle.vue"
export { default as TimelineMeta } from "./TimelineMeta.vue"

export const timelineDotVariants = cva(
  "z-[1] flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 border-newt-border bg-newt-bg-surface text-xs",
  {
    variants: {
      variant: {
        default: "",
        success: "border-newt-online text-newt-online",
        danger: "border-newt-dnd text-newt-dnd",
        brand: "border-newt-brand text-newt-mention-text",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export type TimelineDotVariants = VariantProps<typeof timelineDotVariants>

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { Timeline } from "@/components/ui/timeline"

export function Example() {
  return <Timeline />
}
<script setup lang="ts">
import { Timeline, TimelineItem, TimelineDot, TimelineContent, TimelineTitle, TimelineMeta } from "@/components/ui/timeline"
</script>

<template>
  <Timeline />
</template>

API reference

Timeline

PropType
children / default slot?React.ReactNode / Vue slot

Content rendered inside the component.

TypeReact.ReactNode / Vue slot
className / class?string

Additional classes merged with the component defaults.

Typestring

The registry exports Timeline, TimelineItem, TimelineDot, TimelineContent, TimelineTitle, TimelineMeta. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.

Tokens

Event Timeline reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-timeline.