How to create a Carousel in NextJS?

14,656

Solution 1

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;
    }
  }
}

Solution 2

You can style the buttons by adding css code to your style asset file like .css or scss.

/* class="carousel slide" */
.carousel-control-next {
  /* Find your correct z-index value */
  z-index: 1
}

.carousel-control-prev {
  /* Find your correct z-index value */
  z-index: 1
}

Likewise, you can customize those buttons by adding custom styles.

Share:
14,656
angelo
Author by

angelo

I am a teen developer trying to be like Zuckerberg. I am right now in high School. It has been a year of my coding journey and I loved it.

Updated on June 24, 2022

Comments

  • angelo
    angelo almost 2 years

    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.