Getting Started
App Providerglobal.d.tsThemesTypes
cn(...args)Text ParserUnits Converters
AnchorAvatarBreadcrumbBurgerButtonCardCarouselCheckerCodeColor PickerCommandConfettiCopyButtonDouble Helix WordsFloating IndicatorGroupHighlight TextIndicatorInputKbdLabelLoaderPaginationPassword RequirementPolymorphic SlotProgressProseRatingRunning AreaScroll AreaSheetsSkeletonSliderStackSvgTableTabsTextareaTimelineTimesToasterTooltipTyping WordsTypography
useClickOutsideuseClipboarduseDeviceInfouseDialoguseDidUpdateuseDirectionuseDisclosureuseDocumentTitleuseDocumentVisibilityuseElementInfouseEyeDropperuseFetchuseFullscreenuseGeoLocationuseHotkeysuseHoveruseIduseImagePopupuseInputStateuseIntersectionuseIntervaluseIsomorphicEffectuseListStateuseLocalStorageuseMeasureScrollbaruseMediaQueryuseMergedRefuseMouseuseMoveuseMutationObserveruseNetworkuseOpenStateuseOrientationuseOSusePaginationusePWAInstalleruseRandomColorsuseReducedMotionuseReloaduseResizeObserveruseScrollIntoViewuseStateHistoryuseTimeoutuseTouchuseTriggeruseUncontrolleduseValidatedStateuseViewportSizeuseWindowEventuseWindowScroll
Docs
Web
Components
Timeline
Textarea

A form input for capturing multi-line text with customizable styling and behavior (support JsonInput).

Times

A time utility for presenting or interacting with time values in the UI.


Edit this page on GitHub
  • Started
  • Utilities
  • Configuration
  • Components
  • Hooks
  • Examples
  • Github
  • Contributing
⌘+J

© 2025 oeri rights MIT


Designed in Earth-616

Built by oeri

Timeline

A vertical layout component for displaying chronological events, steps, or milestones with visual clarity.

Usage

Basic usage example to quickly see how the Timeline works.

New branch

Create a new branch to start a new feature or fix bugs.

Posted: 01 Des 24

Commits

Track changes and save versions of your project with meaningful messages.

Posted: 02 Des 24

Pull request

Request to merge your branch into the main branch after completing a feature.

Posted: 03 Des 24

Code review

Collaboratively review changes before they are merged to ensure quality.

Posted: 03 Des 24
import { cnx } from "xuxi";
import { TimesPosted } from "@/ui/times";
import { Tooltip } from "@/ui/tooltip";
import { Timeline } from "@/ui/timeline";
import { GitBranchIcon, GitCommitIcon, GitPullRequestDraftIcon, GitPullRequestIcon } from "@/icons/*";

const data = [
  {
    id: "1",
    title: "New branch",
    description: "Create a new branch to start a new feature or fix bugs.",
    icons: <GitBranchIcon size={12} />,
    createdAt: new Date("2024-12-01T10:30:00").toISOString()
  },
  {
    id: "2",
    title: "Commits",
    description: "Track changes and save versions of your project with meaningful messages.",
    icons: <GitCommitIcon size={12} />,
    createdAt: new Date("2024-12-02T14:45:00").toISOString()
  },
  {
    id: "3",
    title: "Pull request",
    description: "Request to merge your branch into the main branch after completing a feature.",
    icons: <GitPullRequestIcon size={12} />,
    createdAt: new Date("2024-12-03T09:15:00").toISOString()
  },
  {
    id: "4",
    title: "Code review",
    description: "Collaboratively review changes before they are merged to ensure quality.",
    icons: <GitPullRequestDraftIcon size={12} />,
    createdAt: new Date("2024-12-03T11:00:00").toISOString()
  }
];

export function TimelineWithTooltipDemo() {
  return (
    <Timeline.List
      align="left"
      bulletStyle={{ size: 25 }}
      lineStyle={{ width: 3, variant: "double", clr: "hsla(45, 90%, 45%, 0.25)" }}
      className="max-w-5xl py-12 pl-10"
    >
      {data.map(i => {
        const isActive = ["1", "2", "4"].includes(i.id);
        return (
          <Timeline.Item
            key={i.id}
            title={i.title}
            active={isActive}
            activeStyle={{ line: "#fab005" }}
            lineStyle={{ variant: cnx({ dotted: isActive }) }}
            bullet={
              <Tooltip touch withArrow defaultOpen={!isActive} side="top" content={i.title} classNames={{ trigger: "size-full flex items-center justify-center", content: "py-1 px-1.5" }}>
                {i.icons}
              </Tooltip>
            }
            content={
              <>
                <p className="mt-2 text-xs">{i.description}</p>
                <TimesPosted times={{ createdAt: i.createdAt }} className="mt-2 text-xs" />
              </>
            }
          />
        );
      })}
    </Timeline.List>
  );
}

Properties

Interactive configurator to explore customization options for the Timeline component.

New branch

Create a new branch to start a new feature or fix bugs.

01 Desember 2024

Commits

Track changes and save versions of your project with meaningful messages.

02 Desember 2024

Pull request

Request to merge your branch into the main branch after completing a feature.

03 Desember 2024

Code review

Collaboratively review changes before they are merged to ensure quality.

03 Desember 2024
import { Timeline } from "@/ui/timeline";
import { Times } from "@/ui/times";
import { GitBranchIcon, GitCommitIcon, GitPullRequestDraftIcon, GitPullRequestIcon } from "@/icons/*";

const data = [
  {
    id: "1",
    title: "New branch",
    description: "Create a new branch to start a new feature or fix bugs.",
    icons: <GitBranchIcon size={12} className="size-[--sz] [--sz:calc(var(--tl-bullet-size)/2)]" />,
    createdAt: new Date("2024-12-01T10:30:00")
  },
  {
    id: "2",
    title: "Commits",
    description: "Track changes and save versions of your project with meaningful messages.",
    icons: <GitCommitIcon size={12} className="size-[--sz] [--sz:calc(var(--tl-bullet-size)/2)]" />,
    createdAt: new Date("2024-12-02T14:45:00")
  },
  {
    id: "3",
    title: "Pull request",
    description: "Request to merge your branch into the main branch after completing a feature.",
    icons: <GitPullRequestIcon size={12} className="size-[--sz] [--sz:calc(var(--tl-bullet-size)/2)]" />,
    createdAt: new Date("2024-12-03T09:15:00")
  },
  {
    id: "4",
    title: "Code review",
    description: "Collaboratively review changes before they are merged to ensure quality.",
    icons: <GitPullRequestDraftIcon size={12} className="size-[--sz] [--sz:calc(var(--tl-bullet-size)/2)]" />,
    createdAt: new Date("2024-12-03T11:00:00")
  }
];

export function TimelineDemo() {
  return (
    <Timeline bulletStyle={{ round: 8, size: 32 }} lineStyle={{ width: 1 }}>
      {data.map(i => (
        <Timeline.Item key={i.id} title={i.title} active={["1", "2", "4"].includes(i.id)} withNotif={["2", "3"].includes(i.id)} bullet={i.icons} activeStyle={{ line: "hsl(var(--color)/0.6)", ring: "transparent" }}>
          <p className="mt-2 text-xs text-muted-foreground">{i.description}</p>
          <Times time={i.createdAt} className="mt-2 text-xs text-muted-foreground" />
        </Timeline.Item>
      ))}
    </Timeline>
  );
}

Structure

Declarative Props API

import { cnx } from "xuxi";
import { Timeline, TimelineProps } from "@/ui/timeline";
import { GitBranchIcon, GitCommitIcon, GitPullRequestDraftIcon, GitPullRequestIcon } from "@/icons/*";
 
const data = [
  {
    id: "1",
    title: "New branch",
    description: "Create a new branch to start a new feature or fix bugs.",
    icons: <GitBranchIcon size={12} />,
    createdAt: new Date("2024-12-01T10:30:00").toISOString()
  },
  {
    id: "2",
    title: "Commits",
    description: "Track changes and save versions of your project with meaningful messages.",
    icons: <GitCommitIcon size={12} />,
    createdAt: new Date("2024-12-02T14:45:00").toISOString()
  },
  {
    id: "3",
    title: "Pull request",
    description: "Request to merge your branch into the main branch after completing a feature.",
    icons: <GitPullRequestIcon size={12} />,
    createdAt: new Date("2024-12-03T09:15:00").toISOString()
  },
  {
    id: "4",
    title: "Code review",
    description: "Collaboratively review changes before they are merged to ensure quality.",
    icons: <GitPullRequestDraftIcon size={12} />,
    createdAt: new Date("2024-12-03T11:00:00").toISOString()
  }
];
 
const items: TimelineProps["items"] = data.map(i => {
  const isActive = ["3", "4"].includes(i.id);
  return {
    title: i.title,
    active: isActive,
    lineStyle: { variant: cnx({ dotted: isActive }) },
    bullet: i.icons,
    content: (
      <>
        <p className="mt-2 text-xs">{i.description}</p>
        <time dateTime={String(i.createdAt)} className="mt-2 text-xs">
          {String(i.createdAt)}
        </time>
      </>
    )
  };
});
 
export function Demo() {
  return <Timeline items={items} className="max-w-5xl py-12 pl-20 pr-4 md:pl-16" />;
}

Alternative

You can use several ways to use <Timeline/>.

  1. Using with <TimelineList/> and <TimelineItem/> for server component.
export function MyComponent() {
  return (
    <TimelineList>
      <TimelineItem title="Title..." content="Content..."></TimelineItem>
 
      <TimelineItem>
        <h4>Title...</h4>
        <div>Content...</div>
      </TimelineItem>
    </TimelineList>
  );
}
  1. Using with <Timeline/> origin of for use client component.
"use client";
 
export function MyComponent() {
  return (
    <Timeline.List align="right">
      <Timeline.Item title="Title..." content="Content..."></Timeline.Item>
 
      <Timeline.Item>
        <h4>Title...</h4>
        <div>Content...</div>
      </Timeline.Item>
    </Timeline.List>
  );
}

@tailwind utilities

By default, styles set using the tailwindcss class.

Add the following plugins to your tailwind.config.js file:

import { PluginAPI, type Config } from "tailwindcss/types/config";
import plugin from "tailwindcss/plugin";
 
export default {
  theme: {
    extend: {}
  },
  plugins: [
    require("tailwindcss-animate"),
    plugin(({ addBase, addUtilities }: PluginAPI) => {
      addBase({});
      addUtilities({
        ".timeline-item": {
          "--tli-line": "var(--tli-line-width, var(--tl-line-width)) var(--tli-has-line-active-style, var(--tl-line-style)) var(--tli-line-clr, var(--tl-line-clr))",
          "&::before": {
            content: '""',
            position: "absolute",
            top: "var(--tli-line-top, 0)",
            left: "var(--tli-line-left, 0)",
            right: "var(--tli-line-right, 0)",
            bottom: "var(--tli-line-bottom, -2rem)",
            display: "var(--tli-line-display, none)",
            borderInlineStart: "var(--tli-line)",
            pointerEvents: "none"
          },
          "&:where(:not(:first-of-type))": {
            marginTop: "2rem"
          },
          "&:where(:not(:last-of-type))": {
            "--tli-line-display": "block"
          },
          "&:where([data-active]:has(+ [data-active]))": {
            "--tli-has-line-active-style": "var(--tli-line-style)",
            "--tli-has-bullet-active-style": "solid",
            "&::before": {
              borderColor: "var(--active-line, var(--tli-active-line, var(--tl-line-clr)))"
            }
          }
        },
        ".timeline-item-bullet": {
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          position: "absolute",
          top: "var(--tl-bullet-top, calc(var(--tl-bullet-size) * (-16.667 / 100)))",
          width: "var(--tl-bullet-size)",
          height: "var(--tl-bullet-size)",
          borderRadius: "var(--tl-bullet-round)",
          outline: "var(--tli-line-width, var(--tl-line-width)) var(--tli-has-bullet-active-style, var(--tl-bullet-style)) var(--tli-line-clr, var(--tl-bullet-ring, var(--tl-line-clr)))",
          "&[data-active]": {
            outlineColor: "var(--active-ring, var(--bullet-active-ring, var(--tl-line-clr)))"
          },
          "&[data-bullet]": {
            "&[data-active]": {
              backgroundColor: "var(--active-bg, var(--tli-active-bg, var(--tl-line-clr)))",
              "& *": {
                color: "var(--active-clr, var(--tli-active-clr))"
              }
            },
            "& svg": {
              flexShrink: "0",
              pointerEvents: "none"
            }
          },
          "&[data-notif]": {
            "--inset-tr": "calc(var(--tli-line-width, var(--tl-line-width)) * -0.75)",
            "&::after": {
              content: '""',
              zIndex: "0",
              position: "absolute",
              width: "33.333333%",
              height: "33.333333%",
              top: "var(--inset-tr)",
              right: "var(--inset-tr)",
              borderRadius: "inherit",
              backgroundColor: "var(--notif-clr, var(--tli-notif-clr))",
              boxShadow: "0 0 0 calc(var(--tli-line-width, var(--tl-line-width)) / 2) var(--notif-clr, var(--tli-notif-clr)), 0 0 0 calc(var(--tli-line-width, var(--tl-line-width)) / 2 + 2px) var(--notif-ring, var(--tli-notif-ring))"
            }
          }
        }
      });
    })
  ]
} satisfies Config;

API References

TimelineList

Props API TimelineListTypeDefaultAnnotation
align?"left" | "right""left"timeline align position
bulletStyle?{size?: number; round?: number; ring?: Colors; variant?: LineVariant;}{size: 24, round?: 9999, ring: "var(--tl-line-clr)", variant?: "solid"}bulletStyle={{ size: number, round: number, ring: Colors, variant?: LineVariant }}
lineStyle?{width?: number; variant?: LineVariant; clr?: Colors;}{width: 2, variant: "solid", clr: "hsl(var(--constructive))"}lineStyle={{ width: number, variant: LineVariant, clr: Colors }}

TimelineItem

type T = "list" | "bullet" | "body" | "title" | "content";
Props API TimelineItemTypeDefaultAnnotation
lineStyle?{width?: number; variant?: LineVariant; clr?: Colors;}undefinedlineStyle={{ width: number, variant: LineVariant, clr: Colors }}
bullet?React.ReactNodenullcomponents inside bullet, usually for <Icons>
title?React.ReactNodenulltitle declaration to be wrapped in <h4>
content?React.ReactNodenull
classNames?Partial<Record<T, string>>undefinedclassNames={{ list: ""; bullet: ""; body: ""; title: ""; content: "" }}
styles?Partial<Record<T, React.CSSProperties & { [key: string]: any;}>>undefinedstyles={{ list: {}; bullet: {}; body: {}; title: {}; content: {} }}
active?booleanfalse
activeStyle?{bg?: Colors; clr?: Colors; line?: string;}activeStyleactiveStyle={{ bg: "", clr: "", line: "" }}
withNotif?booleanfalse
notifStyle?{clr?: Colors; ring?: Colors;}undefinednotifStyle={{ clr: "", ring: "" }}

Source Codes

Full working code example, including necessary markup and styles. You can copy and paste this code directly to start using the component immediately.

timeline.tsx