An integrations section built like a timeline chart. Six horizontal dashed tracks fill a rounded panel, and small pills, each with an icon and a tool name, glide along them from left to right at their own speed and spacing. A soft clear pool in the middle holds the headline, a paragraph, a link and a pause button, and the pills pass behind it. The motion is one CSS keyframe rule; there is no duplicated list and no script measuring widths. On phones the same six tracks stay, with narrower edge fades.
// Adapted from Eldora UI (https://github.com/karthikmudunuri/eldoraui/blob/main/apps/www/registry/eldoraui/logo-timeline.tsx)
// Copyright (c) Mudunuri Bhaskara Karthikeya Varma. MIT License: https://we.inc/components/licenses
import { useState } from "react";
import type { ComponentType, CSSProperties } from "react";
import { BarChart3, Bell, Calendar, CreditCard, Database, FileText, Inbox, KeyRound, Mail, MessageSquare, Pause, Play, Receipt, Table, Truck, Users, Video, Webhook } from "lucide-react";
type Item = { label: string; icon: ComponentType<{ className?: string }>; row: number; duration: number; delay: number; tint: string };
// `delay` is negative so every pill is already somewhere along its track on the first frame.
const items: Item[] = [
{ label: "Ledgerline", icon: Receipt, row: 1, duration: 38, delay: -4, tint: "text-emerald-600" },
{ label: "Postbox", icon: Mail, row: 1, duration: 38, delay: -17, tint: "text-sky-600" },
{ label: "Rota", icon: Calendar, row: 1, duration: 38, delay: -29, tint: "text-rose-600" },
{ label: "Hookwire", icon: Webhook, row: 2, duration: 52, delay: -8, tint: "text-violet-600" },
{ label: "Tallyboard", icon: BarChart3, row: 2, duration: 52, delay: -25, tint: "text-amber-600" },
{ label: "Huddle", icon: Video, row: 2, duration: 52, delay: -41, tint: "text-teal-600" },
{ label: "Paywell", icon: CreditCard, row: 3, duration: 44, delay: -2, tint: "text-indigo-600" },
{ label: "Gridsheet", icon: Table, row: 3, duration: 44, delay: -16, tint: "text-emerald-600" },
{ label: "Roster", icon: Users, row: 3, duration: 44, delay: -31, tint: "text-orange-600" },
{ label: "Threadbare", icon: MessageSquare, row: 4, duration: 58, delay: -12, tint: "text-sky-600" },
{ label: "Vaultkey", icon: KeyRound, row: 4, duration: 58, delay: -30, tint: "text-slate-700" },
{ label: "Pagemill", icon: FileText, row: 4, duration: 58, delay: -48, tint: "text-rose-600" },
{ label: "Stackbase", icon: Database, row: 5, duration: 40, delay: -6, tint: "text-violet-600" },
{ label: "Nudge", icon: Bell, row: 5, duration: 40, delay: -20, tint: "text-amber-600" },
{ label: "Dropdesk", icon: Inbox, row: 5, duration: 40, delay: -33, tint: "text-teal-600" },
{ label: "Freightly", icon: Truck, row: 6, duration: 50, delay: -15, tint: "text-indigo-600" },
{ label: "Mailroom", icon: Mail, row: 6, duration: 50, delay: -37, tint: "text-emerald-600" },
];
const rows = Array.from(new Set(items.map((i) => i.row))).sort((a, b) => a - b);
export default function MultiRowLogoTimelineWithPillsGlidingAlongDashedTracks() {
const [paused, setPaused] = useState(false);
return (
<section className="bg-white px-4 py-16 sm:px-6 sm:py-20">
<style>{`
@keyframes multi-row-logo-timeline-with-pills-gliding-along-dashed-tracks-move {
from { transform: translate(-100%, -50%); }
to { transform: translate(100cqw, -50%); }
}
.multi-row-logo-timeline-with-pills-gliding-along-dashed-tracks-pill {
animation-name: multi-row-logo-timeline-with-pills-gliding-along-dashed-tracks-move;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@media (prefers-reduced-motion: reduce) {
.multi-row-logo-timeline-with-pills-gliding-along-dashed-tracks-pill { animation-play-state: paused !important; }
}
`}</style>
<div className="relative mx-auto max-w-6xl overflow-hidden rounded-3xl border border-slate-200 bg-slate-50">
{/* The tracks. `container-type` lets the keyframes measure the width of this box with cqw units. */}
<div
aria-hidden="true"
className="absolute inset-0 grid [container-type:inline-size]"
style={{ gridTemplateRows: `repeat(${rows.length}, 1fr)` }}
>
{rows.map((row) => (
<div key={row} className="relative border-b border-slate-200/70 last:border-b-0">
<div className="absolute inset-x-0 top-1/2 h-px bg-[linear-gradient(90deg,rgb(148,163,184)_3px,transparent_3px)] bg-[length:12px_1px] opacity-60" />
{items
.filter((i) => i.row === row)
.map((i) => (
<div
key={i.label}
className="multi-row-logo-timeline-with-pills-gliding-along-dashed-tracks-pill absolute left-0 top-1/2 flex items-center gap-2 whitespace-nowrap rounded-full bg-white py-1.5 pl-2 pr-3.5 shadow-[0_4px_14px_-4px_rgba(15,23,42,0.18)] ring-1 ring-slate-900/10"
style={{ animationDuration: `${i.duration}s`, animationDelay: `${i.delay}s`, animationPlayState: paused ? "paused" : "running" } as CSSProperties}
>
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-slate-100">
<i.icon className={`h-3.5 w-3.5 ${i.tint}`} />
</span>
<span className="text-sm font-medium text-slate-800">{i.label}</span>
</div>
))}
</div>
))}
</div>
{/* Edge fades and a clear pool behind the copy. */}
<div className="pointer-events-none absolute inset-y-0 left-0 w-16 bg-gradient-to-r from-slate-50 to-transparent sm:w-28" aria-hidden="true" />
<div className="pointer-events-none absolute inset-y-0 right-0 w-16 bg-gradient-to-l from-slate-50 to-transparent sm:w-28" aria-hidden="true" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(110%_38%_at_50%_50%,rgb(248,250,252)_62%,rgba(248,250,252,0.7)_82%,transparent)] sm:bg-[radial-gradient(46%_42%_at_50%_50%,rgb(248,250,252)_45%,rgba(248,250,252,0.7)_72%,transparent)]" aria-hidden="true" />
<div className="relative flex min-h-[680px] flex-col items-center justify-center px-5 py-16 sm:min-h-[540px] sm:px-6 text-center">
<p className="text-sm font-semibold uppercase tracking-widest text-indigo-600">60 connections and counting</p>
<h2 className="mt-4 max-w-xl text-3xl font-semibold tracking-tight text-slate-900 sm:text-5xl">Plugs into the tools your week already runs on</h2>
<p className="mt-4 max-w-md text-base leading-relaxed text-slate-600 sm:mt-5 sm:text-lg">
Orderly reads from your inbox, calendar, billing and chat, then keeps one tidy timeline of what happened and who needs to act.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<a href="#" className="inline-flex h-12 items-center rounded-full bg-slate-900 px-6 text-sm font-semibold text-white transition-colors hover:bg-indigo-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2">
Browse all connections
</a>
<button
type="button"
onClick={() => setPaused((p) => !p)}
aria-pressed={paused}
className="inline-flex h-12 items-center gap-2 rounded-full border border-slate-300 bg-white px-5 text-sm font-semibold text-slate-800 transition-colors hover:bg-slate-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2"
>
{paused ? <Play className="h-4 w-4" aria-hidden="true" /> : <Pause className="h-4 w-4" aria-hidden="true" />}
{paused ? "Play motion" : "Pause motion"}
</button>
</div>
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/multi-row-logo-timeline-with-pills-gliding-along-dashed-tracks.json
The tracks container has `container-type: inline-size`, which makes the `cqw` unit equal to one percent of its width. The keyframes move each pill from fully off the left edge to `100cqw`, which is exactly the right edge, whatever the screen size.
A negative delay starts the animation part-way through. Without it every pill would begin off screen on the left and the panel would be empty for the first few seconds.
A marquee repeats one strip in a loop. Here each pill is its own element with its own speed and start point, so rows drift past each other and the pattern never visibly repeats. It also means there is no duplicated content in the HTML.
Yes. Replace the lucide icon inside the small circle with an inline SVG or an `img` of the same size. Keep the text label beside it so the pill stays readable at small sizes.
They are supported in Chrome and Edge 105, Safari 16 and Firefox 110 and later. In an older browser the pills stay at the left edge, so consider the plain logo row as a fallback if you support those.
Adapted from Logo Timeline in Eldora UI, used under its open-source licence. Licences and credits
Related: Dual Row Logo Marquee with Edge Fade, Animated Beam Connecting Integrations, Logo Cloud with Drawn Wordmarks.
More marquees or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.