Retour au catalogue
Timeline Alternating
Timeline en zigzag avec images et descriptions alternees gauche-droite. Format editorial immersif.
timelinemedium Both Responsive a11y
editorialelegantuniversalagencyportfoliosplit
Theme
"use client";
import { motion } from "framer-motion";
import { ArrowUpRight } from "lucide-react";
interface TimelineEvent {
id: string;
date: string;
title: string;
description: string;
image?: string;
ctaLabel?: string;
ctaUrl?: string;
}
interface TimelineAlternatingProps {
badge?: string;
title?: string;
subtitle?: string;
events: TimelineEvent[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function TimelineAlternating({
badge,
title = "Notre parcours",
subtitle,
events = [],
}: TimelineAlternatingProps) {
return (
<section
className="py-[var(--section-padding-y,6rem)]"
style={{ backgroundColor: "var(--color-background-alt)" }}
>
<div className="mx-auto max-w-6xl px-[var(--container-padding-x,1.5rem)]">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: EASE }}
className="text-center max-w-2xl mx-auto mb-20"
>
{badge && (
<span
className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border mb-4"
style={{ color: "var(--color-accent)", borderColor: "var(--color-border)" }}
>
{badge}
</span>
)}
{title && (
<h2
className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl"
style={{ color: "var(--color-foreground)" }}
>
{title}
</h2>
)}
{subtitle && (
<p className="mt-4 text-base" style={{ color: "var(--color-foreground-muted)" }}>
{subtitle}
</p>
)}
</motion.div>
{/* Zigzag events */}
<div className="space-y-24 md:space-y-32">
{events.map((event, i) => {
const isEven = i % 2 === 0;
return (
<motion.div
key={event.id}
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.7, ease: EASE }}
className={`flex flex-col gap-8 md:gap-16 md:items-center ${
isEven ? "md:flex-row" : "md:flex-row-reverse"
}`}
>
{/* Image placeholder */}
<div className="flex-1">
<div
className="aspect-[4/3] rounded-[var(--radius-xl,1.5rem)] overflow-hidden"
style={{
backgroundColor: "color-mix(in srgb, var(--color-accent) 8%, var(--color-background))",
border: "1px solid var(--color-border)",
}}
>
{event.image ? (
<img
src={event.image}
alt={event.title}
className="w-full h-full object-cover"
/>
) : (
<div
className="w-full h-full flex items-center justify-center text-6xl font-bold opacity-20"
style={{ color: "var(--color-accent)" }}
>
{String(i + 1).padStart(2, "0")}
</div>
)}
</div>
</div>
{/* Content */}
<div className="flex-1">
<span
className="inline-block text-sm font-bold tracking-wider uppercase mb-3"
style={{ color: "var(--color-accent)" }}
>
{event.date}
</span>
<h3
className="text-2xl md:text-3xl font-bold tracking-tight mb-4"
style={{ color: "var(--color-foreground)" }}
>
{event.title}
</h3>
<p
className="text-base leading-relaxed mb-6"
style={{ color: "var(--color-foreground-muted)" }}
>
{event.description}
</p>
{event.ctaLabel && (
<a
href={event.ctaUrl || "#"}
className="inline-flex items-center gap-1.5 text-sm font-semibold transition-opacity duration-200 hover:opacity-80"
style={{ color: "var(--color-accent)" }}
>
{event.ctaLabel}
<ArrowUpRight className="w-4 h-4" />
</a>
)}
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}