I encountered a similar problem in my project and decided to modify my code based on your approach.
Here is the MainComponent:
'use client'
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { ChevronLeft, ChevronRight } from "lucide-react"
export default function MainComponent() {
const [activeCardIndex, setActiveCardIndex] = useState(0)
const cardsContainerRef = useRef<HTMLDivElement>(null)
const cardsRefs = useRef<(HTMLDivElement | null)[]>([])
const updateLayoutOffset = useCallback(() => {
if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
return
}
const cardWidth = cardsRefs.current[activeCardIndex]!.offsetWidth
const containerWidth = cardsContainerRef.current.offsetWidth
let offset = containerWidth / 2 - cardWidth / 2 - activeCardIndex * cardWidth
cardsContainerRef.current.style.transform = `translateX(${offset}px)`
}, [activeCardIndex])
useLayoutEffect(() => {
updateLayoutOffset()
}, [updateLayoutOffset])
useEffect(() => {
updateLayoutOffset()
window.addEventListener('resize', updateLayoutOffset)
return () => window.removeEventListener('resize', updateLayoutOffset)
}, [activeCardIndex, updateLayoutOffset])
const goToNextCard = useCallback(() => {
setActiveCardIndex((prev) => Math.min(prev + 1, cardsRefs.current.length - 1))
}, [])
const goToPreviousCard = useCallback(() => {
setActiveCardIndex((prev) => Math.max(prev - 1, 0))
}, [])
const cards = ["Card 1", "Card 2", "Card 3"]
return (
<div className="flex h-dvh max-h-dvh w-full max-w-full flex-col justify-center overflow-hidden bg-background px-2 py-1 sm:py-3">
<div className="flex h-full max-h-full flex-row items-center gap-4">
<Button
variant="outline"
size="icon"
onClick={goToPreviousCard}
disabled={activeCardIndex === 0}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="relative flex aspect-[9/16] h-full max-h-full max-w-full flex-col overflow-hidden rounded-md bg-card">
<div
ref={cardsContainerRef}
className="flex h-full transition-transform duration-500 ease-in-out"
>
{cards.map((card, index) => (
<Card
key={index}
ref={(el) => (cardsRefs.current[index] = el)}
className="h-full w-full shrink-0 overflow-hidden"
>
<CardHeader className="h-16">
<h2 className="text-xl font-semibold">{card}</h2>
</CardHeader>
<CardContent className="h-[calc(100%-8rem)] overflow-y-auto">
<div className="space-y-2">
{Array(40).fill(0).map((_, i) => (
<p key={i} className="text-center">
{card} Content {i + 1}
</p>
))}
</div>
</CardContent>
<CardFooter className="h-16">
<Input className="w-full" placeholder="Type a message..." />
</CardFooter>
</Card>
))}
</div>
</div>
<Button
variant="outline"
size="icon"
onClick={goToNextCard}
disabled={activeCardIndex === cards.length - 1}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)
}
Button.tsx Component:
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button";
export { Button, buttonVariants }
Input.tsx component:
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input";
export { Input }
Card.tsx component:
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }