Retour au catalogue
Product Showcase Carousel
Carousel de produits avec image large, navigation fleches et dots. Transition animee entre produits.
product-showcasemedium Both Responsive a11y
minimalelegantecommerceuniversalcarousel
Theme
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface Product {
id: string;
name: string;
price: string;
imageSrc?: string;
imageAlt?: string;
tag?: string;
}
interface ProductShowcaseCarouselProps {
badge?: string;
title?: string;
subtitle?: string;
products?: Product[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function ProductShowcaseCarousel({
badge = "Collection",
title = "Nos produits",
subtitle = "Decouvrez notre selection",
products = [],
}: ProductShowcaseCarouselProps) {
const [current, setCurrent] = useState(0);
const prev = () => setCurrent((c) => (c === 0 ? products.length - 1 : c - 1));
const next = () => setCurrent((c) => (c === products.length - 1 ? 0 : c + 1));
if (products.length === 0) return null;
return (
<section
style={{
padding: "var(--section-padding-y-lg) 0",
background: "var(--color-background)",
overflow: "hidden",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: EASE }}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-end",
marginBottom: "3rem",
flexWrap: "wrap",
gap: "1rem",
}}
>
<div>
{badge && (
<span
style={{
display: "inline-block",
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
color: "var(--color-accent)",
marginBottom: "0.5rem",
}}
>
{badge}
</span>
)}
<h2
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.75rem, 3vw, 2.75rem)",
fontWeight: 700,
letterSpacing: "-0.02em",
color: "var(--color-foreground)",
marginBottom: "0.5rem",
}}
>
{title}
</h2>
<p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)" }}>
{subtitle}
</p>
</div>
{/* Navigation arrows */}
<div style={{ display: "flex", gap: "0.5rem" }}>
<button
onClick={prev}
aria-label="Produit precedent"
style={{
width: 44,
height: 44,
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-border)",
background: "var(--color-background)",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "var(--color-foreground-muted)",
transition: "all var(--duration-normal) var(--ease-out)",
}}
>
<ChevronLeft style={{ width: 20, height: 20 }} />
</button>
<button
onClick={next}
aria-label="Produit suivant"
style={{
width: 44,
height: 44,
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-accent)",
background: "var(--color-accent)",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "var(--color-foreground)",
transition: "all var(--duration-normal) var(--ease-out)",
}}
>
<ChevronRight style={{ width: 20, height: 20 }} />
</button>
</div>
</motion.div>
{/* Carousel */}
<div style={{ position: "relative", minHeight: "380px" }}>
<AnimatePresence mode="wait">
<motion.div
key={current}
initial={{ opacity: 0, x: 60 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -60 }}
transition={{ duration: 0.45, ease: EASE }}
style={{
display: "grid",
gridTemplateColumns: "1fr",
gap: "2rem",
alignItems: "center",
}}
className="md:!grid-cols-[1.2fr_1fr]"
>
{/* Image */}
<div
style={{
borderRadius: "var(--radius-xl)",
overflow: "hidden",
aspectRatio: "4/3",
background: "var(--color-background-alt)",
position: "relative",
}}
>
{products[current].imageSrc ? (
<img
src={products[current].imageSrc}
alt={products[current].imageAlt || products[current].name}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
) : (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: `radial-gradient(circle, color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 60%)`,
color: "var(--color-foreground-muted)",
fontSize: "0.875rem",
}}
>
{products[current].name}
</div>
)}
{products[current].tag && (
<span
style={{
position: "absolute",
top: "1rem",
left: "1rem",
padding: "0.35rem 0.75rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "var(--color-foreground)",
fontSize: "0.75rem",
fontWeight: 600,
}}
>
{products[current].tag}
</span>
)}
</div>
{/* Info */}
<div>
<h3
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.5rem, 2.5vw, 2.25rem)",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "0.75rem",
}}
>
{products[current].name}
</h3>
<span
style={{
fontSize: "1.5rem",
fontWeight: 700,
color: "var(--color-accent)",
}}
>
{products[current].price}
</span>
</div>
</motion.div>
</AnimatePresence>
</div>
{/* Dots */}
<div
style={{
display: "flex",
justifyContent: "center",
gap: "0.5rem",
marginTop: "2rem",
}}
>
{products.map((_, i) => (
<button
key={i}
onClick={() => setCurrent(i)}
aria-label={`Voir produit ${i + 1}`}
style={{
width: i === current ? 32 : 8,
height: 8,
borderRadius: "var(--radius-full)",
background: i === current ? "var(--color-accent)" : "var(--color-border)",
border: "none",
cursor: "pointer",
transition: "all var(--duration-normal) var(--ease-out)",
}}
/>
))}
</div>
</div>
</section>
);
}
Autres variantes product-showcase
Product Showcase 360
complex · both
boldelegant
Product Showcase Comparison
complex · both
corporateminimal
Product Showcase Exploded
complex · both
boldcorporate
Product Showcase Features
medium · both
minimalcorporate
Product Showcase Hero
medium · both
minimalelegant
Product Showcase Scroll
complex · both
minimalcorporate