Lädt...


🔧 How to implement a slider element using React, Tailwind.css and Intersection Observer API


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

Let’s see how we could implement an image slider (a.k.a. carousel, a.k.a. slideshow) using React, Tailwind.css and as much browser-native API as possible if your designer has provided you with the following design:

The design
The design

Project setup

From now on I’ll assume that you project is already set up. If that’s not the case, I would recommend following Next.js Getting Started Guide and Next.js Styling With Tailwind, these two should give you the necessary foundation.

Creating the component

First of all, let’s create our component, it will be receiving a list of image URLs as an argument and render each of them in a loop:

// src/components/image-slider.jsx

export default function ImageSlider({ images }) {
  return (
    <div>
      {images.map((url) => {
        return (
          <div id={url}>
            <img src={url} />
          </div>
        );
      })}
    </div>
  );
}

We also want to be able to see it, so let’s render the ImageSlider component at the main page and let’s use Lorem Picsum to create a list of placeholder image URLs:

// src/app/page.js

import ImageSlider from "../components/image-slider";

export default function Page() {
  return (
    <>
      <h1>{"Image slider"}</h1>
      <ImageSlider
        images={[
          "https://picsum.photos/id/10/960/540",
          "https://picsum.photos/id/11/960/540",
          "https://picsum.photos/id/12/960/540",
        ]}
      />
    </>
  );
}

The result so far should look like this:

Slides stacked on top of each otherSlides stacked on top of each other

Horizontal slider look

Let’s make it look a bit more like a slider using Tailwind’s flexbox utilities. We need to add flex flex-row overflow-x-scroll classes to the slider and w-full flex-shrink-0 classes to each slide:

// src/components/image-slider.jsx

export default function ImageSlider({ images }) {
  return (
    <div className="w-full flex flex-row overflow-x-scroll">
      {images.map((url) => {
        return (
          <div key={url} className="w-full flex-shrink-0">
            <img src={url} />
          </div>
        );
      })}
    </div>
  );
}

We’ll also add a container to the page, so that the slider does not take the whole page, which is probably not something that you want anyway:

// src/app/page.js

import ImageSlider from "../components/image-slider";

export default function Page() {
  return (
    <div className="w-full max-w-xl mx-auto">
      {/* ... */}
    </div>
  );
}

Now you should see something like this:

Slides look good but don’t snapSlides look good but don’t snap

Snapping

Cool, this is definitely a slider, but there’s a catch: it won’t snap to pictures when you scroll:

Slides look good but don’t snapSlides look good but don’t snap

Let’s fix that by adding snap-x snap-mandatory classes to the slider component and snap-start class to each slide:

// src/components/image-slider.jsx

export default function ImageSlider({ images }) {
  return (
    <div className="w-full flex flex-row overflow-x-scroll snap-x snap-mandatory">
      {images.map((url) => {
        return (
          <div key={url} className="w-full flex-shrink-0 snap-start">
            <img src={url} />
          </div>
        );
      })}
    </div>
  );
}

Now it should feel much better:

Slides do snapSlides do snap

Custom scroll indicator

Now we could call it a day here, but our design also requires a custom scrollbar, or rather a custom scroll position indicator, remember that little thing?

The little thing (scroll indicator)The little thing (scroll indicator)

Let’s add a wrapper element for the slider, so that we could also add the indicator, and the indicator itself (the indicator width in percent is going to be 100 divided by the number of images):

// src/components/image-slider.jsx

export default function ImageSlider({ images }) {
  const indicatorWidthPercent = images.length > 0 ? 100 / images.length : 100;

  return (
    <div className="w-full">
      {/* ... */}
      <div className="w-full h-0.5 relative bg-gray-300">
        <div
          className="h-0.5 absolute top-0 left-0 bg-gray-500"
          style={{ width: `${indicatorWidthPercent}%` }}
        />
      </div>
    </div>
  );
}

Now it should look like this:

Double scrollbarDouble scrollbar

Hiding the default scrollbar

Awesome, but that looks kinda ugly, why would we need two indicators? Let’s hide the default one by adding a padding to the slider and clipping the default scrollbar area:

// src/components/image-slider.jsx

export default function ImageSlider({ images }) {
  const indicatorWidthPercent = images.length > 0 ? 100 / images.length : 100;

  return (
    <div className="w-full">
      <div
        className="w-full flex flex-row overflow-x-scroll snap-x snap-mandatory"
        style={{
          paddingBottom: "15px",
          clipPath: "inset(0 0 15px 0)",
        }}
      >
        {/* ... */}
      </div>
      {/* ... */}
    </div>
  );
}

The default scrollbar is hidden now:

Custom scroll indicator is visible but does not moveCustom scroll indicator is visible but does not move

Animating the custom scroll indicator

Of course, we also want it to display the actual slide selection and not just be stuck at the first position. Currently there seems to be no way to achieve this other than either using the Intersection Observer API or listening to scroll events on the slider. We will go with the former, because it’s much cleaner (the latter would require to basically implement the Intersection Observer API behavior on our own) and arguably more performant, since it’s a native API:

// src/components/image-slider.jsx

"use client";

import { useState, useRef, useEffect } from "react";

export default function ImageSlider({ images }) {
  const indicatorWidthPercent = images.length > 0 ? 100 / images.length : 100;

  const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
  const sliderRef = useRef(null);

  useEffect(() => {
    const sliderCurrent = sliderRef.current;

    if (!sliderCurrent) {
      return;
    }

    // Find all the slides inside of the slider
    const slides = sliderCurrent.querySelectorAll("div");
    const slidesArray = Array.from(slides);

    // Wait until a slide is 50% visible, then find it's index in the array of
    // all slides and update the currentSlideIndex
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const index = slidesArray.indexOf(entry.target);
            setCurrentSlideIndex(index);
          }
        });
      },
      {
        root: sliderCurrent,
        threshold: 0.5,
      }
    );
    slides.forEach((slide) => observer.observe(slide));

    return () => slides.forEach((slide) => observer.unobserve(slide));
  }, []);

  return (
    <div className="w-full">
      {/* Slider */}
      <div
        ref={sliderRef}
        className="w-full flex flex-row overflow-x-scroll snap-x snap-mandatory"
        style={{
          paddingBottom: "15px",
          clipPath: "inset(0 0 15px 0)",
        }}
      >
        {images.map((url) => {
          return (
            <div key={url} className="w-full flex-shrink-0 snap-start">
              <img src={url} />
            </div>
          );
        })}
      </div>

      {/* Scroll indicator */}
      <div className="w-full h-0.5 relative bg-gray-300">
        <div
          className="h-0.5 absolute top-0 left-0 bg-gray-500"
          style={{
            width: `${indicatorWidthPercent}%`,
            left: `${indicatorWidthPercent * currentSlideIndex}%`,
            transition: "left 150ms ease-in-out",
          }}
        />
      </div>
    </div>
  );
}

That should be it:

The final resultThe final result

Considerations

Such clipping of the default scrollbar will obviously make the component unusable on a desktop without a mac touch pad, but adding a couple of navigation buttons on either side is not difficult. Another option would be listening to click events on the scroll indicator, getting the click position and scrolling to the according slide, but the indicator width might need to be adjusted, as it’s only 2px now.

There might be accessibility issues if the scrollbar is clipped, but this again could be solved by adding a button to scroll to the left and another one to scroll to the right.

The component will break if JavaScript is disabled in the user’s browser. One way to mitigate this would be to add a noscript tag and un-clip the bottom of the slider in it via CSS:

padding-bottom: 0px !important; 
clip-path: none !important;

Feedback

You can find the source code here:

GitHub logo prutya / tutorial-image-slider

How to implement a slider element using React, Tailwind.css and Intersection Observer API

01_image-slider






If you have any feedback, please feel free to submit an Issue.

My original blog post

...

🔧 How to implement a slider element using React, Tailwind.css and Intersection Observer API


📈 115.44 Punkte
🔧 Programmierung

🔧 How to Implement Reveal on Scroll in React using the Intersection Observer API


📈 66.78 Punkte
🔧 Programmierung

🔧 How to animate objects on scroll with Tailwind CSS and the JavaScript intersection observer API


📈 65.07 Punkte
🔧 Programmierung

🔧 How to animate objects on scroll with Tailwind CSS and the JavaScript intersection observer API


📈 65.07 Punkte
🔧 Programmierung

🔧 How to Create Infinite Scrolling in React Using the Intersection Observer API


📈 54.36 Punkte
🔧 Programmierung

🔧 How to Implement Infinite Scroll in Next.js with Intersection Observer


📈 49.11 Punkte
🔧 Programmierung

🔧 Effortless Infinite Scrolling: How to Implement Lazy Loading with Intersection Observer


📈 49.11 Punkte
🔧 Programmierung

🔧 Simple Guide to Using Intersection Observer API with ReactJS


📈 47.29 Punkte
🔧 Programmierung

🔧 Infinite Scrolling Demo with Intersection Observer, React, and `useRef`


📈 45.43 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust [16] Implement React Noop


📈 45.38 Punkte
🔧 Programmierung

🔧 Utilizing Intersection Observer in React


📈 43.76 Punkte
🔧 Programmierung

🔧 How to Add Scroll Animations to a Page with JavaScript's Intersection Observer API


📈 42.55 Punkte
🔧 Programmierung

🔧 Infinite Scrolling: Mastering the Intersection Observer API By Making Seamless Infinite Scroll like a Pro


📈 42.55 Punkte
🔧 Programmierung

🔧 Infinite Scrolling: Mastering the Intersection Observer API By Making Seamless Infinite Scroll like a Pro


📈 42.55 Punkte
🔧 Programmierung

🔧 Understanding the Intersection Observer API


📈 42.55 Punkte
🔧 Programmierung

🔧 How to implement a Multi-Select Dropdown component with React and Tailwind CSS


📈 42.01 Punkte
🔧 Programmierung

🔧 Enhance Your React Applications with cards-slider-react-lib : A Feature-Rich Card Slider Library


📈 42 Punkte
🔧 Programmierung

🔧 Creating a Pricing Table with Range Slider using Tailwind CSS and Alpine.js


📈 41.19 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [13] Implement Lane and Batch Update


📈 39.98 Punkte
🔧 Programmierung

🔧 How to implement dark mode using Tailwind and React JS?


📈 38.36 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [10] Implement Update for Single Node.


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [11] Implement Event System


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [12] Implement Update for Multi Node


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [14] Implement Scheduler


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [17] Implement Concurrent Mode


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [18] Implement useRef, useCallback, useMemo


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [20] Implement Context


📈 38.31 Punkte
🔧 Programmierung

🔧 Implement React v18 from Scratch Using WASM and Rust - [22] Implement memo


📈 38.31 Punkte
🔧 Programmierung

🔧 Tailwind CSS vs. Traditional CSS in a React app: Pros, Cons, and Best Use Cases


📈 37.98 Punkte
🔧 Programmierung

🔧 Choosing the Right CSS Approach: Tailwind CSS vs Bootstrap vs Vanilla CSS


📈 37.63 Punkte
🔧 Programmierung

🔧 Building an Infinite Scroll Component with Intersection Observer 🚀


📈 36.69 Punkte
🔧 Programmierung

🔧 Mastering Intersection Observer: Enhance Your Web Experiences


📈 36.69 Punkte
🔧 Programmierung

🔧 Enhancing Website Performance with Intersection Observer


📈 36.69 Punkte
🔧 Programmierung

🔧 Intersection Observer: A powerful tool for efficient web design 🚀


📈 36.69 Punkte
🔧 Programmierung

🔧 Intersection Observer: Ein mächtiges Tool für effizientes Web-Design 🚀


📈 36.69 Punkte
🔧 Programmierung

matomo