Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSS Scroll Snap Points with navigation (next, previous) buttons

I am building a carousel, very minimalist, using CSS snap points. It is important for me to have CSS only options, but I'm fine with enhancing a bit with javascript (no framework).

I am trying to add previous and next buttons to scroll programmatically to the next or previous element. If javascript is disabled, buttons will be hidden and carousel still functionnal.

My issue is about how to trigger the scroll to the next snap point ?

All items have different size, and most solution I found require pixel value (like scrollBy used in the exemple). A scrollBy 40px works for page 2, but not for others since they are too big (size based on viewport).

function goPrecious() {
  document.getElementById('container').scrollBy({ 
    top: -40,
    behavior: 'smooth' 
  });
}

function goNext() {
  document.getElementById('container').scrollBy({ 
    top: 40,
    behavior: 'smooth' 
  });
}
#container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;

  border: 2px solid var(--gs0);
  border-radius: 8px;
  height: 60vh;
}

#container div {
  scroll-snap-align: start;

  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 4rem;
}
#container div:nth-child(1) {
  background: hotpink;
  color: white;
  height: 50vh;
}
#container div:nth-child(2) {
  background: azure;
  height: 40vh;
}
#container div:nth-child(3) {
  background: blanchedalmond;
  height: 60vh;
}
#container div:nth-child(4) {
  background: lightcoral;
  color: white;
  height: 40vh;
}
<div id="container">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
</div>

<button onClick="goPrecious()">previous</button>
<button onClick="goNext()">next</button>
like image 981
sebastienbarbier Avatar asked Aug 16 '19 02:08

sebastienbarbier


People also ask

What is scroll snap align in CSS?

The scroll-snap-align property specifies the box's snap position as an alignment of its snap area (as the alignment subject) within its snap container's snapport (as the alignment container). The two values specify the snapping alignment in the block axis and inline axis, respectively.


2 Answers

An easier approach done with react.

export const AppCarousel = props => {

  const containerRef = useRef(null);
  const carouselRef = useRef(null);


  const [state, setState] = useState({
    scroller: null,
    itemWidth: 0,
    isPrevHidden: true,
    isNextHidden: false

  })

  const next = () => {
    state.scroller.scrollBy({left: state.itemWidth * 3, top: 0, behavior: 'smooth'});

    // Hide if is the last item
    setState({...state, isNextHidden: true, isPrevHidden: false});
  }


   const prev = () => {
    state.scroller.scrollBy({left: -state.itemWidth * 3, top: 0, behavior: 'smooth'});
    setState({...state, isNextHidden: false, isPrevHidden: true});
    // Hide if is the last item
    // Show remaining
   }

  useEffect(() => {

      const items = containerRef.current.childNodes;
      const scroller = containerRef.current;
      const itemWidth = containerRef.current.firstElementChild?.clientWidth;

      setState({...state, scroller, itemWidth});

    return () => {

    }
  },[props.items])


  return (<div className="app-carousel" ref={carouselRef}>

      <div className="carousel-items shop-products products-swiper" ref={containerRef}>
          {props.children}
      </div>
      <div className="app-carousel--navigation">
        <button className="btn prev" onClick={e => prev()} hidden={state.isPrevHidden}>&lt;</button>
        <button className="btn next" onClick={e => next()} hidden={state.isNextHidden}>&gt;</button>
      </div>

  </div>)
}

like image 89
Jobizzness Avatar answered Sep 19 '22 20:09

Jobizzness


I've just done something similar recently. The idea is to use IntersectionObserver to keep track of which item is in view currently and then hook up the previous/next buttons to event handler calling Element.scrollIntoView().

Anyway, Safari does not currently support scroll behavior options. So you might want to polyfill it on demand with polyfill.app service.

let activeIndex = 0;
const container = document.querySelector("#container");
const elements = [...document.querySelectorAll("#container div")];

function handleIntersect(entries){
  const entry = entries.find(e => e.isIntersecting);
  if (entry) {
    const index = elements.findIndex(
      e => e === entry.target
    );
    activeIndex = index;
  }
}

const observer = new IntersectionObserver(handleIntersect, {
  root: container,
  rootMargin: "0px",
  threshold: 0.75
});

elements.forEach(el => {
  observer.observe(el);
});

function goPrevious() {
  if(activeIndex > 0) {
    elements[activeIndex - 1].scrollIntoView({
      behavior: 'smooth'
    })
  }
}

function goNext() {
  if(activeIndex < elements.length - 1) {
    elements[activeIndex + 1].scrollIntoView({
      behavior: 'smooth'
    })
  }
}
#container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;

  border: 2px solid var(--gs0);
  border-radius: 8px;
  height: 60vh;
}

#container div {
  scroll-snap-align: start;

  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 4rem;
}
#container div:nth-child(1) {
  background: hotpink;
  color: white;
  height: 50vh;
}
#container div:nth-child(2) {
  background: azure;
  height: 40vh;
}
#container div:nth-child(3) {
  background: blanchedalmond;
  height: 60vh;
}
#container div:nth-child(4) {
  background: lightcoral;
  color: white;
  height: 40vh;
}
<div id="container">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
</div>

<button onClick="goPrevious()">previous</button>
<button onClick="goNext()">next</button>
like image 33
donysukardi Avatar answered Sep 20 '22 20:09

donysukardi