1import { Timeline } from "~/components/ui/timeline";2
3export default function TimelineDemo() {4 return (5 <Timeline6 items={[7 {8 title: "Event #1",9 description: "This is the first event of the timeline.",10 },11 {12 title: "Event #2",13 description: "This is the second event of the timeline.",14 },15 {16 title: "Event #3",17 description: "This is the third event of the timeline.",18 },19 ]}20 activeItem={1}21 />22 );23}
npx shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/timeline.json
yarn shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/timeline.json
pnpm dlx shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/timeline.json
bunx --bun shadcn@latest add https://solid-ui-neobrutalism.vercel.app/r/timeline.json
Copy and paste the following code into your project
1import type {2 Component,3 ComponentProps,4 JSXElement,5 ParentComponent,6} from "solid-js";7
8import { For, mergeProps, Show, splitProps } from "solid-js";9
10import { cn } from "~/lib/utils";11
12export type TimelinePropsItem = Omit<13 TimelineItemProps,14 "isActive" | "isActiveBullet" | "bulletSize" | "lineSize"15> & {16 bulletSize?: number;17};18
19export type TimelineProps = {20 items: TimelinePropsItem[];21 activeItem: number;22 bulletSize?: number;23 lineSize?: number;24};25
26/*27 No bullet or line is active when activeItem is -128 First bullet is active only if activeItem is 0 or more29 First line is active only if activeItem is 1 or more30*/31
32const Timeline: Component<TimelineProps> = (rawProps) => {33 const props = mergeProps({ bulletSize: 16, lineSize: 2 }, rawProps);34
35 return (36 <ul37 data-slot="timeline"38 style={{39 "padding-left": `${props.bulletSize / 2}px`,40 }}41 >42 <For each={props.items}>43 {(item, index) => (44 <TimelineItem45 title={item.title}46 description={item.description}47 bullet={item.bullet}48 isLast={index() === props.items.length - 1}49 isActive={50 props.activeItem === -1 ? false : props.activeItem >= index() + 151 }52 isActiveBullet={53 props.activeItem === -1 ? false : props.activeItem >= index()54 }55 bulletSize={props.bulletSize}56 lineSize={props.lineSize}57 />58 )}59 </For>60 </ul>61 );62};63
64export type TimelineItemProps = {65 title: JSXElement;66 description?: JSXElement;67 bullet?: JSXElement;68 isLast?: boolean;69 isActive: boolean;70 isActiveBullet: boolean;71 class?: string;72 bulletSize: number;73 lineSize: number;74};75
76const TimelineItem: Component<TimelineItemProps> = (props) => {77 const [local, others] = splitProps(props, [78 "class",79 "bullet",80 "description",81 "title",82 "isLast",83 "isActive",84 "isActiveBullet",85 "bulletSize",86 "lineSize",87 ]);88 return (89 <li90 data-slot="timeline-item"91 class={cn(92 "relative border-l-primary/30 pb-8 pl-8",93 local.isLast && "border-l-transparent pb-0",94 local.isActive && !local.isLast && "border-l-primary",95 local.class,96 )}97 style={{98 "border-left-width": `${local.lineSize}px`,99 }}100 {...others}101 >102 <TimelineItemBullet103 lineSize={local.lineSize}104 bulletSize={local.bulletSize}105 isActive={local.isActiveBullet}106 >107 {local.bullet}108 </TimelineItemBullet>109 <TimelineItemTitle>{local.title}</TimelineItemTitle>110 <Show when={local.description}>111 <TimelineItemDescription>{local.description}</TimelineItemDescription>112 </Show>113 </li>114 );115};116
117export type TimelineItemBulletProps = {118 children?: JSXElement;119 isActive?: boolean;120 bulletSize: number;121 lineSize: number;122};123
124const TimelineItemBullet: Component<TimelineItemBulletProps> = (props) => {125 return (126 <div127 data-slot="timeline-item-bullet"128 class={cn(129 `absolute top-0 flex items-center justify-center rounded-full border border-primary/30 bg-background`,130 props.isActive && "border-primary",131 )}132 style={{133 width: `${props.bulletSize}px`,134 height: `${props.bulletSize}px`,135 left: `${-props.bulletSize / 2 - props.lineSize / 2}px`,136 "border-width": `${props.lineSize}px`,137 }}138 aria-hidden="true"139 >140 {props.children}141 </div>142 );143};144
145const TimelineItemTitle: ParentComponent = (props) => {146 return (147 <div148 data-slot="timeline-item-title"149 class="mb-1 text-base leading-none font-semibold"150 >151 {props.children}152 </div>153 );154};155
156const TimelineItemDescription: Component<ComponentProps<"p">> = (props) => {157 const [local, others] = splitProps(props, ["class", "children"]);158 return (159 <p160 data-slot="timeline-item-description"161 class={cn("text-sm text-muted-foreground", local.class)}162 {...others}163 >164 {local.children}165 </p>166 );167};168
169export { Timeline };
Update the import paths to match your project setup.
1import { Timeline } from "~/components/ui/timeline";
1<Timeline2 items={[3 {4 title: "Event #1",5 description: "This is the first event of the timeline.",6 },7 {8 title: "Event #2",9 description: "This is the second event of the timeline.",10 },11 {12 title: "Event #3",13 description: "This is the third event of the timeline.",14 },15 ]}16 activeItem={1}17/>