skip to Main Content

I am trying to create a scroll container for cards where the scroll is performed in the card content only, not in the header or footer. I guess that the container should hide the horizontal overflow and the card should have overflow-y: auto. However, doing so causes that the following cards are not visible.

function MainComponent() {
  const [activeCardIndex, setActiveCardIndex] = React.useState(0);

  const cardsContainerRef = React.useRef(null);
  const cardsRefs = React.useRef([]);

  const updateLayoutOffset = React.useCallback(() => {
    if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
      return 0;
    }

    const storyWidth = cardsRefs.current[activeCardIndex].offsetWidth;
    const containerWidth = cardsContainerRef.current.offsetWidth;
    let offset = containerWidth / 2 - storyWidth / 2 - activeCardIndex * storyWidth - activeCardIndex * 8;
    cardsContainerRef.current.style.transform = `translateX(${offset}px)`;
  }, [activeCardIndex]);

  React.useLayoutEffect(() => {
    updateLayoutOffset();
  }, [updateLayoutOffset]);

  React.useEffect(() => {
    updateLayoutOffset();
  }, [activeCardIndex, updateLayoutOffset]);

  const goToNextCard = React.useCallback(() => {
    if (activeCardIndex === cardsRefs.current.length - 1) {
      return;
    }

    setActiveCardIndex(activeCardIndex + 1);
  }, [activeCardIndex]);

  const goToPreviousCard = React.useCallback(() => {
    if (activeCardIndex === 0) {
      return;
    }

    setActiveCardIndex(activeCardIndex - 1);
  }, [activeCardIndex]);

  let 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-black px-2 py-1 sm:bg-[#1a1a1a] sm:py-3">
      <div className="flex h-full max-h-full flex-row transition-transform duration-500">
        <div className="flex h-full max-w-full flex-row items-center sm:gap-4">
          <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToPreviousCard}>
            Previous
          </button>
          <div className="flex aspect-[9/16] h-full max-h-full max-w-full flex-col rounded-md bg-black">
            <div className="flex flex-col gap-2 p-2">
              <h1 className="h-fit flex-1 text-left leading-none text-white">Header </h1>
            </div>
            <div ref={cardsContainerRef} className="flex grow flex-row items-center gap-2 overflow-y-auto overflow-x-clip transition-transform duration-500">
              {cards.map((card, index) => (
                <div
                  ref={(el) => {
                    if (el) {
                      cardsRefs.current[index] = el;
                    }
                  }}
                  key={index}
                  className="relative flex h-full min-w-full max-w-full flex-col gap-2 overflow-y-auto text-pretty rounded-md bg-white p-2 dark:bg-[#343434]"
                >
                  {Array(40)
                    .fill(0)
                    .map((_, i) => (
                      <p key={i} className="text-center">
                        {card}
                      </p>
                    ))}
                </div>
              ))}
            </div>
            <div className="flex w-full flex-row items-center p-2">
              <input className="min-w-0 flex-1 rounded-full border-[1px] bg-transparent px-4 py-2 text-left" />
            </div>
          </div>
          <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToNextCard}>
            Next
          </button>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<MainComponent />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

I can not find any combination of overflow settings that works fine.

2

Answers


  1. Solution 1:

    • Removed overflow-x: clip from the cards container and replaced it
      with overflow-visible to ensure other cards are visible
    • Added a nested scrollable container inside each card:
    <div className="h-full overflow-y-auto">
     {/* Card content */}
    </div>
    
    • Moved the overflow-y-auto from the card itself to this inner container
    import React from 'react';
    
    const MainComponent = () => {
      const [activeCardIndex, setActiveCardIndex] = React.useState(0);
      const cardsContainerRef = React.useRef(null);
      const cardsRefs = React.useRef([]);
    
      const updateLayoutOffset = React.useCallback(() => {
        if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
          return 0;
        }
    
        const storyWidth = cardsRefs.current[activeCardIndex].offsetWidth;
        const containerWidth = cardsContainerRef.current.offsetWidth;
        let offset = containerWidth / 2 - storyWidth / 2 - activeCardIndex * storyWidth - activeCardIndex * 8;
        cardsContainerRef.current.style.transform = `translateX(${offset}px)`;
      }, [activeCardIndex]);
    
      React.useLayoutEffect(() => {
        updateLayoutOffset();
      }, [updateLayoutOffset]);
    
      React.useEffect(() => {
        updateLayoutOffset();
      }, [activeCardIndex, updateLayoutOffset]);
    
      const goToNextCard = React.useCallback(() => {
        if (activeCardIndex === cardsRefs.current.length - 1) return;
        setActiveCardIndex(activeCardIndex + 1);
      }, [activeCardIndex]);
    
      const goToPreviousCard = React.useCallback(() => {
        if (activeCardIndex === 0) return;
        setActiveCardIndex(activeCardIndex - 1);
      }, [activeCardIndex]);
    
      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-black px-2 py-1 sm:bg-[#1a1a1a] sm:py-3">
          <div className="flex h-full max-h-full flex-row transition-transform duration-500">
            <div className="flex h-full max-w-full flex-row items-center sm:gap-4">
              <button 
                className="rounded-md bg-blue-500 p-2 text-white hover:bg-blue-600 transition-colors"
                onClick={goToPreviousCard}
              >
                Previous
              </button>
              
              <div className="flex aspect-[9/16] h-full max-h-full max-w-full flex-col rounded-md bg-black">
                {/* Fixed Header */}
                <div className="flex flex-col gap-2 p-2 bg-black">
                  <h1 className="h-fit flex-1 text-left leading-none text-white">Header</h1>
                </div>
                
                {/* Scrollable Cards Container */}
                <div 
                  ref={cardsContainerRef} 
                  className="flex grow flex-row items-start gap-2 overflow-visible transition-transform duration-500"
                >
                  {cards.map((card, index) => (
                    <div
                      ref={(el) => {
                        if (el) cardsRefs.current[index] = el;
                      }}
                      key={index}
                      className="relative flex h-full min-w-full max-w-full flex-col gap-2 rounded-md bg-white p-2 dark:bg-[#343434]"
                    >
                      {/* Scrollable Content Container */}
                      <div className="h-full overflow-y-auto">
                        {Array(40).fill(0).map((_, i) => (
                          <p key={i} className="text-center py-1">
                            {card}
                          </p>
                        ))}
                      </div>
                    </div>
                  ))}
                </div>
                
                {/* Fixed Footer */}
                <div className="flex w-full flex-row items-center p-2 bg-black">
                  <input 
                    className="min-w-0 flex-1 rounded-full border-[1px] border-gray-600 bg-transparent px-4 py-2 text-left text-white focus:outline-none focus:border-blue-500" 
                    placeholder="Type here..."
                  />
                </div>
              </div>
              
              <button 
                className="rounded-md bg-blue-500 p-2 text-white hover:bg-blue-600 transition-colors"
                onClick={goToNextCard}
              >
                Next
              </button>
            </div>
          </div>
        </div>
      );
    };
    
    export default MainComponent;
    

    Solution 2:

    • Use overflow-y-auto only on the inner content of each card instead of
      the card container. This will allow vertical scrolling only for each
      card’s inner content without affecting the main card container’s
      layout.
    • Set overflow-x-hidden on the main container (cardsContainerRef). This
      will ensure any overflowing content horizontally doesn’t affect the
      rest of the layout.
    function MainComponent() {
      const [activeCardIndex, setActiveCardIndex] = React.useState(0);
    
      const cardsContainerRef = React.useRef(null);
      const cardsRefs = React.useRef([]);
    
      const updateLayoutOffset = React.useCallback(() => {
        if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
          return;
        }
    
        const cardWidth = cardsRefs.current[activeCardIndex].offsetWidth;
        const containerWidth = cardsContainerRef.current.offsetWidth;
        const offset = containerWidth / 2 - cardWidth / 2 - activeCardIndex * cardWidth - activeCardIndex * 8;
        cardsContainerRef.current.style.transform = `translateX(${offset}px)`;
      }, [activeCardIndex]);
    
      React.useLayoutEffect(() => {
        updateLayoutOffset();
      }, [updateLayoutOffset]);
    
      React.useEffect(() => {
        updateLayoutOffset();
      }, [activeCardIndex, updateLayoutOffset]);
    
      const goToNextCard = React.useCallback(() => {
        if (activeCardIndex === cardsRefs.current.length - 1) return;
        setActiveCardIndex(activeCardIndex + 1);
      }, [activeCardIndex]);
    
      const goToPreviousCard = React.useCallback(() => {
        if (activeCardIndex === 0) return;
        setActiveCardIndex(activeCardIndex - 1);
      }, [activeCardIndex]);
    
      const cards = ["Card 1", "Card 2", "Card 3"];
    
      return (
        <div className="flex h-screen max-h-screen w-full max-w-full flex-col justify-center overflow-hidden bg-black px-2 py-1 sm:bg-[#1a1a1a] sm:py-3">
          <div className="flex h-full max-h-full flex-row transition-transform duration-500">
            <div className="flex h-full max-w-full flex-row items-center sm:gap-4">
              <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToPreviousCard}>
                Previous
              </button>
              <div className="flex aspect-[9/16] h-full max-h-full max-w-full flex-col rounded-md bg-black">
                <div className="flex flex-col gap-2 p-2">
                  <h1 className="h-fit flex-1 text-left leading-none text-white">Header</h1>
                </div>
                <div
                  ref={cardsContainerRef}
                  className="flex grow flex-row items-center gap-2 overflow-hidden transition-transform duration-500"
                >
                  {cards.map((card, index) => (
                    <div
                      ref={(el) => {
                        if (el) cardsRefs.current[index] = el;
                      }}
                      key={index}
                      className="relative flex h-full min-w-full max-w-full flex-col gap-2 text-pretty rounded-md bg-white p-2 dark:bg-[#343434]"
                    >
                      <div className="overflow-y-auto max-h-full">
                        {Array(40)
                          .fill(0)
                          .map((_, i) => (
                            <p key={i} className="text-center">
                              {card}
                            </p>
                          ))}
                      </div>
                    </div>
                  ))}
                </div>
                <div className="flex w-full flex-row items-center p-2">
                  <input className="min-w-0 flex-1 rounded-full border-[1px] bg-transparent px-4 py-2 text-left" />
                </div>
              </div>
              <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToNextCard}>
                Next
              </button>
            </div>
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
  2. I had same challenge in my project and I tried to change my code based on your implementation.

    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 }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search