Logo Timeline
Logo Timeline is a React and Tailwind CSS data component from Eldora UI, licensed MIT. Copy it and paste it into your project.
Eldora UI
MIT
data
What it is
Logo Timeline is a React and Tailwind CSS data component from Eldora UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i react motion
Licence
This component comes from Eldora UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Logo Timeline
"use client"
import type React from "react"
import { useState } from "react"
import { motion } from "motion/react"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
export interface LogoItem {
/** The label text displayed next to the icon */
label: string
/** The icon name from the Icons object (e.g., "gitHub", "react", "tailwind") */
icon: keyof typeof Icons
/** Animation delay in seconds (use negative values for staggered effect) */
animationDelay: number
/** Animation duration in seconds */
animationDuration: number
/** The row number where this logo should appear (1-based) */
row: number
}
export interface LogoTimelineProps {
/** Array of logo items to display */
items: LogoItem[]
/** Optional title text to display in the center */
title?: string
/** Height of the timeline container */
height?: string
/** Additional className for the container */
className?: string
/** Icon size in pixels (default: 16) */
iconSize?: number
/** Whether to show separator lines between rows (default: true) */
showRowSeparator?: boolean
/** Whether to animate logos only on hover (default: false) */
animateOnHover?: boolean
}
export function LogoTimeline({
items,
title,
height = "h-[400px] sm:h-[800px]",
className,
iconSize = 16,
showRowSeparator = true,
animateOnHover = false,
}: LogoTimelineProps) {
const [isHovered, setIsHovered] = useState(false)
// Group items by row
const rowsMap = new Map<number, LogoItem[]>()
items.forEach((item) => {
if (!rowsMap.has(item.row)) {
rowsMap.set(item.row, [])
}
rowsMap.get(item.row)?.push(item)
})
// Convert map to sorted array
const rows = Array.from(rowsMap.entries())
.sort(([a], [b]) => a - b)
.map(([, rowItems]) => rowItems)
// Determine animation play state
const animationPlayState = animateOnHover
? isHovered
? "running"
: "paused"
: "running"
const getIconComponent = (iconName: keyof typeof Icons) => {
const Icon = Icons[iconName]
if (!Icon) {
return null
}
return (
<Icon
className="shrink-0"
style={{ width: iconSize, height: iconSize }}
aria-hidden="true"
/>
)
}
return (
<section className={cn("w-full", height, className)}>
<motion.div
aria-hidden="true"
className="bg-background relative h-full w-full overflow-hidden py-24 ring-inset sm:py-32"
onMouseEnter={() => animateOnHover && setIsHovered(true)}
onMouseLeave={() => animateOnHover && setIsHovered(false)}
>
{title && (
<div className="absolute top-1/2 left-1/2 mx-auto w-full max-w-[90%] -translate-x-1/2 -translate-y-1/2 text-center">
<div className="relative z-10">
<p className="text-foreground/10 mx-auto mt-2 max-w-3xl text-4xl font-semibold tracking-tight text-pretty sm:text-5xl md:text-6xl">
{title}
</p>
</div>
</div>
)}
<div
className="[container-type:inline-size] absolute inset-0 grid"
style={{ gridTemplateRows: `repeat(${rows.length}, 1fr)` }}
>
{rows.map((rowItems, index) => (
<div className="group relative flex items-center" key={index}>
<div className="from-foreground/15 dark:from-foreground/15 absolute inset-x-0 top-1/2 h-0.5 bg-gradient-to-r from-[2px] to-[2px] bg-[length:12px_100%]" />
{showRowSeparator && (
<div className="from-foreground/5 dark:from-foreground/5 absolute inset-x-0 bottom-0 h-0.5 bg-gradient-to-r from-[2px] to-[2px] bg-[length:12px_100%] group-last:hidden" />
)}
{rowItems.map((logo) => {
const IconComponent = getIconComponent(logo.icon)
if (!IconComponent) {
return null
}
return (
<div
key={`${logo.row}-${logo.label}`}
className={cn(
"absolute top-1/2 flex -translate-y-1/2 items-center gap-2 px-3 py-1.5 whitespace-nowrap",
"from-secondary/50 to-secondary/50 ring-background/10 dark:ring-foreground/10 rounded-full bg-gradient-to-t from-50% ring-1 backdrop-blur-sm ring-inset",
"[--move-x-from:-100%] [--move-x-to:calc(100%+100cqw)] [animation-iteration-count:infinite] [animation-name:move-x] [animation-timing-function:linear]",
"shadow-[0_0_15px_rgba(0,0,0,0.1)] dark:shadow-none"
)}
style={{
animationDelay: `${logo.animationDelay}s`,
animationDuration: `${logo.animationDuration}s`,
animationPlayState: animationPlayState,
}}
>
{IconComponent}
<span className="text-foreground text-sm/6 font-medium">
{logo.label}
</span>
</div>
)
})}
</div>
))}
</div>
</motion.div>
</section>
)
}