Search code examples
reactjscarouselnext.jsreact-bootstrap

How to create a Carousel in NextJS?


I am trying to build a carousel just like how it is on the amazon home page. I used React Bootstrap for the carousel but it does not seems to work. It just stacks every item just like in a column.

<Carousel>
                <Carousel.Item>
                    <img src={'https://images-na.ssl-images-amazon.com/images/G/01/AmazonExports/Fuji/2020/October/Fuji_Tallhero_Dash_en_US_1x._CB418727898_.jpg'} alt="" />
                </Carousel.Item>

                <Carousel.Item>
                    <img src={'https://images-na.ssl-images-amazon.com/images/G/01/AmazonExports/Events/2020/PrimeDay/Fuji_TallHero_NonPrime_v2_en_US_1x._CB403670067_.jpg'} alt="" />
                </Carousel.Item>
            </Carousel>

Code with any other framework other than React Bootstrap is accepted in the answers.


Solution

  • I just implemented keen slider on a project I'm doing for a client (dentist) in about an hour or so. Using it for patient testimonials. Not too bad, vercel does a nice job of isolating the heavy lifting to a single components subdirectory.

    import { useKeenSlider } from 'keen-slider/react';
    import React, {
        Children,
        FC,
        isValidElement,
        useState,
        useEffect,
        useRef
    } from 'react';
    import cn from 'classnames';
    import css from './keen-slider.module.css';
    
    const KeenSlider: FC = ({ children }) => {
        const [currentSlide, setCurrentSlide] = useState(0);
        const [isMounted, setIsMounted] = useState(false);
        const sliderContainerRef = useRef<HTMLDivElement>(null);
        const [ref, slider] = useKeenSlider<HTMLDivElement>({
            loop: true,
            slidesPerView: 1,
            mounted: () => setIsMounted(true),
            slideChanged(s) {
                setCurrentSlide(s.details().relativeSlide);
            }
        });
        // Stop the history navigation gesture on touch devices
        useEffect(() => {
            const preventNavigation = (event: TouchEvent) => {
                // Center point of the touch area
                const touchXPosition = event.touches[0].pageX;
                // Size of the touch area
                const touchXRadius = event.touches[0].radiusX || 0;
                // We set a threshold (10px) on both sizes of the screen,
                // if the touch area overlaps with the screen edges
                // it's likely to trigger the navigation. We prevent the
                // touchstart event in that case.
                if (
                    touchXPosition - touchXRadius < 10 ||
                    touchXPosition + touchXRadius > window.innerWidth - 10
                )
                    event.preventDefault();
            };
            sliderContainerRef.current!.addEventListener('touchstart', preventNavigation);
            return () => {
                sliderContainerRef.current!.removeEventListener(
                    'touchstart',
                    preventNavigation
                );
            };
        }, []);
    
        return (
            <div className={css.root} ref={sliderContainerRef}>
                <button
                    className={cn(css.leftControl, css.control)}
                    onClick={slider?.prev}
                    aria-label='Previous Testimonial'
                />
                <button
                    className={cn(css.rightControl, css.control)}
                    onClick={slider?.next}
                    aria-label='Next Testimonial'
                />
                <div
                    ref={ref}
                    className='keen-slider h-full transition-opacity duration-150'
                    style={{ opacity: isMounted ? 1 : 0 }}
                >
                    {Children.map(children, child => {
                        // Add the keen-slider__slide className to children
                        if (isValidElement(child)) {
                            return {
                                ...child,
                                props: {
                                    ...child.props,
                                    className: `${
                                        child.props.className ? `${child.props.className} ` : ''
                                    }keen-slider__slide`
                                }
                            };
                        }
                        return child;
                    })}
                </div>
                {slider && (
                    <div className={cn(css.positionIndicatorsContainer)} ref={ref}>
                        {[...Array(slider.details().size).keys()].map(idx => {
                            return (
                                <button
                                    aria-label='Position indicator'
                                    key={idx}
                                    className={cn(css.positionIndicator + `keen-slider__slide`, {
                                        [css.positionIndicatorActive]: currentSlide === idx
                                    })}
                                    onClick={() => {
                                        slider.moveToSlideRelative(idx);
                                    }}
                                >
                                    <div className={css.dot} />
                                </button>
                            );
                        })}
                    </div>
                )}
            </div>
        );
    };
    
    export default KeenSlider;
    
    

    The corresponding css file

    .root {
        @apply relative w-full h-full;
        overflow-y: hidden;
    }
    
    .leftControl,
    .rightControl {
        @apply absolute top-1/2 -translate-x-1/2 z-20 w-16 h-16 flex items-center justify-center bg-hover-1 rounded-full;
    }
    
    .leftControl {
        @apply bg-cover left-10;
        background-image: url('/cursor-left.png');
    }
    
    .rightControl {
        @apply bg-cover right-10;
        background-image: url('/cursor-right.png');
    }
    .leftControl:hover,
    .rightControl:hover {
        @apply bg-hover-2 outline-none shadow-outline-blue;
    }
    
    .control {
        @apply opacity-0 transition duration-150;
    }
    
    .root:hover .control {
        @apply opacity-100;
    }
    
    .positionIndicatorsContainer {
        @apply hidden;
    
        @screen md {
            @apply block absolute  left-1/2;
            transform: translateX(-50%);
        }
    }
    
    .positionIndicator {
        @apply rounded-full p-2;
    }
    
    .dot {
        @apply bg-hover-1 transition w-3 h-3 rounded-full;
    }
    .positionIndicatorActive .dot {
        @apply bg-white;
    }
    
    .positionIndicator:hover .dot {
        @apply bg-hover-2;
    }
    
    .positionIndicator:focus {
        @apply outline-none;
    }
    
    .positionIndicator:focus .dot {
        @apply shadow-outline-blue;
    }
    
    .positionIndicatorActive:hover .dot {
        @apply bg-white;
    }
    
    .number-slide {
        background: grey;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 50px;
        color: #fff;
        font-weight: 500;
        height: 300px;
        max-height: 100vh;
    }
    
    

    My implementation

    import cn from 'classnames';
    import { Container } from '@components/UI';
    import TestimonialsData from './TestimonialsData';
    import TestimonialsWrapper from './TestimonialsWrapper';
    import dynamic from 'next/dynamic';
    import { ALL_TESTIMONIALS } from '@lib/graphql';
    import { useQuery } from '@apollo/client';
    import {
        PostObjectsConnectionOrderbyEnum,
        OrderEnum
    } from '@_types/graphql-global-types';
    import {
        AllTestimonials,
        AllTestimonialsVariables
    } from '@lib/graphql/AllTestimonials/__generated__/AllTestimonials';
    import css from './testimonials.module.css';
    import KeenSlider from '../KeenSlider/keen-slider';
    
    export const TestimonialsQueryVars: AllTestimonialsVariables = {
        first: 10,
        order: OrderEnum.ASC,
        field: PostObjectsConnectionOrderbyEnum.TITLE
    };
    
    const LoadingDots = dynamic(() => import('@components/UI/LoadingDots'));
    
    const Loading = () => (
        <div className='w-80 h-80 flex items-center text-center justify-center p-3'>
            <LoadingDots />
        </div>
    );
    
    const dynamicProps = {
        loading: () => <Loading />
    };
    
    const ApolloErrorMessage = dynamic(
        () => import('@components/ErrorMessage'),
        dynamicProps
    );
    
    const TestimonialsCoalesced = () => {
        const { loading, error, data } = useQuery<
            AllTestimonials,
            AllTestimonialsVariables
        >(ALL_TESTIMONIALS, {
            variables: TestimonialsQueryVars,
            notifyOnNetworkStatusChange: true
        });
    
        return error ? (
            <>
                <ApolloErrorMessage
                    message={`${error.message}`}
                    graphQLErrors={error.graphQLErrors}
                    networkError={error.networkError}
                    extraInfo={error.extraInfo}
                    stack={error.stack}
                    name={error.name}
                />
            </>
        ) : loading && !error ? (
            <Loading />
        ) : (
            <Container className={cn('mx-auto max-w-none w-full')} clean>
                {data &&
                data.prosites !== null &&
                data.prosites.edges !== null &&
                data.prosites.edges.length > 0 ? (
                    <TestimonialsWrapper root={css.sliderContainer}>
                        <KeenSlider>
                            {data.prosites.edges.map(edge => {
                                return edge !== null && edge.cursor !== null && edge.node !== null ? (
                                    <div className={css.childContainer}>
                                        <TestimonialsData
                                            root={''}
                                            key={edge.node.id}
                                            id={edge.node.id}
                                            __typename={edge.node.__typename}
                                            title={edge.node.title}
                                            slug={edge.node.slug}
                                            featuredImage={edge.node.featuredImage}
                                            content={edge.node.content}
                                            modified={edge.node.modified}
                                        />
                                    </div>
                                ) : (
                                    <div>{error}</div>
                                );
                            })}
                        </KeenSlider>
                    </TestimonialsWrapper>
                ) : (
                    <div>{error}</div>
                )}
            </Container>
        );
    };
    
    export default TestimonialsCoalesced;
    
    

    The CSS from my testimonials implementation

    .sliderContainer {
      @apply absolute z-10 inset-0 flex items-center justify-center overflow-x-hidden;
    }
    
    .childContainer {
      & > div {
        @apply h-full;
        & > div {
          @apply h-full;
        }
      }
    }