Docs
Slide Cards
Slide Cards
A dynamic component that reveals hidden cards while scrolling.
Scroll To animate
Hear it from our clients
Outstanding Design & Development
Unizoy revamped our website with a modern, responsive design that boosted our online presence and conversions.
AW
Alice W.
Exceptional Customer Support
Their support team was prompt and professional, guiding us through every step to ensure smooth implementation.
MT
Mark T.
Innovative Digital Solutions
Unizoy provided creative digital strategies that transformed our marketing approach and drove measurable growth.
CS
Claire S.
Reliable & Professional
Working with Unizoy has been a pleasure—they consistently deliver quality work on time, every time.
JD
James D.
Install the following dependencies:
pnpm add gsap react-icons
Make a file for cn function and match the import afterwards
import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes))
Make a file and copy paste this code in a file with name Slide Cards
"use client"
import React, { useRef, RefObject, useState, useEffect } from "react"
import { gsap } from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import { useGSAP } from "@gsap/react"
import { cn } from "@/lib/utils"
interface ItemsProps {
title: string
description: string
name: string
background?: string
}
interface SlidingCardsProps {
items: ItemsProps[]
maxWidth?: number
gap?: number
backgroundImage?: string
triggerStart?: string
triggerEnd?: string
markers?: boolean // Toggle markers on/off
headingText?: string // Optional heading text. If omitted, heading won't render.
minHeightValue?: number // Optional minimum height for card content
parentClassName?: string
parentContentClassName?: string
headingClassName?: string
contentClassName?: string
scrollerRef?: RefObject<HTMLElement>
}
const SlidingCards: React.FC<SlidingCardsProps> = ({
items,
maxWidth = 340,
gap = 10, // Added gap value (in pixels)
backgroundImage = "https://www.jeton.com/_ipx/f_webp&q_80&w_3400/cms/b7c674ecd0ee69b2eca20443cac6272c550ed396-4000x2667.jpg",
triggerStart = "top 30%",
triggerEnd = "+=800",
markers = false,
headingText = "Hear it from our clients",
minHeightValue,
parentClassName = "",
parentContentClassName = "",
headingClassName = "",
contentClassName = "",
scrollerRef,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const instanceIdRef = useRef<string>(
`rotating-text-${Math.random().toString(36).substring(2, 11)}`
)
const [forceUpdate, setForceUpdate] = useState(false)
useEffect(() => {
console.log("from useEffext")
if (scrollerRef?.current) {
setForceUpdate(!forceUpdate)
}
}, [scrollerRef?.current])
useGSAP(() => {
gsap.registerPlugin(ScrollTrigger)
if (!containerRef.current) return
const existingTrigger = ScrollTrigger.getById(instanceIdRef.current)
if (existingTrigger) {
existingTrigger.kill()
}
const cards = containerRef.current.querySelectorAll(".card")
const cardsArray = Array.from(cards)
// Get initial heights of each card's inner content wrapper
// Alternative: Calculate content-only height by subtracting vertical padding
const initialHeights = cardsArray.map((card) => {
const content = card.querySelector(".card-content") as HTMLElement
if (!content) return 0
return content.getBoundingClientRect().height
})
console.log("initialHeights", initialHeights)
// const minHeight = Math.min(...initialHeights)
const minHeight =
minHeightValue !== undefined
? minHeightValue
: Math.min(...initialHeights)
console.log("minHeight", minHeight)
// Create a timeline with ScrollTrigger
const tl = gsap.timeline({
scrollTrigger: {
trigger: containerRef.current,
start: triggerStart,
end: triggerEnd,
scrub: true,
markers: markers,
scroller: scrollerRef?.current ?? window,
id: instanceIdRef.current,
},
})
// Animate each card's inner content wrapper height and translation, adding a gap.
cardsArray.forEach((card, index) => {
if (index === 0) return
const subset = cardsArray.slice(index)
const content = subset[0].querySelector(".card-content") as HTMLElement
if (!content) return
// Add the gap to the translation amount
tl.to(subset, {
y: `+=${initialHeights[index] + gap}`, // gap added here
duration: 1, // Increased duration for smoother motion
ease: "sine.out",
stagger: 0.1, // Stagger each subset animation slightly
})
.to(
subset[0],
{
width: maxWidth,
duration: 1.5,
ease: "power2.out",
},
"<"
)
.fromTo(
content,
{ height: minHeight },
{ height: initialHeights[index], duration: 1.5, ease: "power2.out" },
"<"
)
})
}, [forceUpdate])
return (
<div
ref={containerRef}
style={{
backgroundImage: `url(${backgroundImage})`,
}}
className={cn(
"relative h-[150vh] flex flex-col justify-start items-center bg-cover bg-center",
parentClassName
)}
>
<h1
className={cn(
"text-[4.5rem] text-white my-20 font-semibold",
headingClassName
)}
>
{headingText}
</h1>
<div
className={cn(
"relative flex flex-col justify-center items-center mt-20",
parentContentClassName
)}
>
{items.map((item, index) => {
const width = maxWidth - index * 20
return (
<div
key={index}
className="absolute flex flex-col p-4 m-2 font-grotesk gap-2 card rounded overflow-hidden"
style={{
bottom: `${index * -8}px`,
width: `${width}px`,
zIndex: items.length - index,
}}
>
{/* Modified: Wrap card content inside an inner container and align content to bottom
so that if height is reduced, the bottom remains visible */}
<div
className="card-content overflow-hidden flec flex-col max-h-fit"
style={{
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
}}
>
<div className={cn("child", contentClassName)}>
<div className="flex flex-col">
<h1 className="text-white font-semibold">{item.title}</h1>
<p className="text-white/70">{item.description}</p>
</div>
<div className="flex gap-2 text-white items-center">
<div
className={`flex justify-center items-center w-9 h-9 rounded-full p-2 ${item.background}`}
>
<p className="text-[#360802]">
{item.name
.split(" ")
.map((word) => word[0])
.join("")}
</p>
</div>
<p>{item.name}</p>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
)
}
export { SlidingCards }
Copy this css and paste in global css
.card {
backdrop-filter: blur(30px) saturate(100%);
-webkit-backdrop-filter: blur(rotate-3) saturate(100%);
background-color: rgba(255, 255, 255, 0.15);
border-radius: 12px;
}
Provide an array of items with the following properties: title, description, name, and background.
const items = [
{
title: "Outstanding Design & Development",
description: "Unizoy revamped our website with a modern, responsive design that boosted our online presence and conversions.",
name: "Alice W.",
background: "bg-[#FFDCB8]",
},
{
title: "Exceptional Customer Support",
description: "Their support team was prompt and professional, guiding us through every step to ensure smooth implementation.",
name: "Mark T.",
background: "bg-[#BBD2FF]",
},
...
...
...
];
Props
Name | Type | Description |
---|---|---|
items | ItemsProps[] | Array of slide card items with title, description, name, and background properties. |
maxWidth | number | Maximum width of a card. |
gap | number | Gap value added between card animations. Default is 30 . |
backgroundImage | string | Background image URL for the slide container. |
triggerStart | string | ScrollTrigger start position. |
triggerEnd | string | ScrollTrigger end position/duration. |
markers | boolean | Toggle for ScrollTrigger markers. Default is false . |
headingText | string | Optional heading text for the slide cards. |
minHeightValue | number | Minimum height value for the card content. |
parentClassName | string | Additional classes for the slide container. |
parentContentClassName | string | Additional classes for the card content wrapper. |
headingClassName | string | Additional classes for the heading element. |
contentClassName | string | Additional classes for the card inner content. |
scrollerRef | RefObject<HTMLElement> | Reference to the scrolling container element. |
Credit: This component is inspired by Jeton.