Skip to content

Instantly share code, notes, and snippets.

@benknight
Last active August 16, 2023 04:59
Show Gist options
  • Save benknight/3bbf8dbcbb0dfef9adc611be74538f67 to your computer and use it in GitHub Desktop.
Save benknight/3bbf8dbcbb0dfef9adc611be74538f67 to your computer and use it in GitHub Desktop.
[use-carousel] Headless UI React hook for building a scroll-based carousel

[use-carousel] Headless UI React hook for building a scroll-based carousel

BYO-UI. No CSS necessary. Inspired by react-table.

Usage:

const {
  getLeftNavProps,
  getRightNavProps,
  isTouchDevice,
  navigate,
  scrollAreaRef,
  scrollPosition,
  showNav,
} = useCarousel();

Use CSS to avoid unwanted scrollbars using the following technique:

parent {
  overflow: hidden;
}

child {
  overflow-x: auto;
  margin-bottom: -16px;
  padding-bottom: 16px;
}

Instance properties

Property Type Description
getLeftNavProps function Returns props for left arrow button
getRightNavProps function Returns props for right arrow button
isTouchDevice Boolean Whether the user is using a touch device or not, useful for hiding the navigate for touch users who can just swipe
navigate function(delta: Int) Navigates by a specified delta e.g. 1 or -1
scrollAreaRef ref Reference to be assigned to the scroll parent
scrollPosition string Describes current scroll position as "start", "end", or "between"
showNav Boolean false if there aren't enough items to scroll
import { useRef, useState, useCallback, useEffect } from 'react';
export default function useCarousel() {
const scrollArea = useRef();
const [isTouchDevice, setIsTouchDevice] = useState(null);
const [scrollBy, setScrollBy] = useState(null);
const [scrollPosition, setScrollPosition] = useState(null);
const [showNav, setShowNav] = useState(null);
const navigate = useCallback(
delta => {
const { scrollLeft } = scrollArea.current;
scrollArea.current.scroll({
behavior: 'smooth',
left: scrollLeft + scrollBy * delta,
});
},
[scrollBy],
);
useEffect(() => {
const scrollAreaNode = scrollArea.current;
const calculateScrollPosition = () => {
if (!scrollAreaNode) return;
const { width } = scrollAreaNode.getBoundingClientRect();
if (scrollAreaNode.scrollLeft === 0) {
setScrollPosition('start');
} else if (
scrollAreaNode.scrollLeft + width ===
scrollAreaNode.scrollWidth
) {
setScrollPosition('end');
} else {
setScrollPosition('between');
}
};
// Calculate scrollBy offset
const calculateScrollBy = () => {
if (!scrollAreaNode) return;
const { width: containerWidth } = scrollAreaNode.getBoundingClientRect();
setShowNav(scrollAreaNode.scrollWidth > containerWidth);
const childNode = scrollAreaNode.querySelector(':scope > *');
if (!childNode) return;
const { width: childWidth } = childNode.getBoundingClientRect();
setScrollBy(childWidth * Math.floor(containerWidth / childWidth));
};
const observer = new MutationObserver(calculateScrollBy);
const attachListeners = () => {
if (scrollAreaNode) observer.observe(scrollAreaNode, { childList: true });
scrollAreaNode.addEventListener('scroll', calculateScrollPosition);
window.addEventListener('resize', calculateScrollBy);
};
const detachListeners = () => {
observer.disconnect();
scrollAreaNode.removeEventListener('scroll', calculateScrollPosition);
window.removeEventListener('resize', calculateScrollBy);
};
if (isTouchDevice === true) {
detachListeners();
}
if (isTouchDevice === false) {
attachListeners();
calculateScrollBy();
calculateScrollPosition();
}
return detachListeners;
}, [isTouchDevice, navigate]);
useEffect(() => {
const mql = window.matchMedia('(pointer: fine)');
const handleMql = ({ matches }) => {
setIsTouchDevice(!matches);
};
handleMql(mql);
mql.addEventListener('change', handleMql);
return () => {
mql.removeEventListener('change', handleMql);
};
}, []);
return {
getLeftNavProps: () => ({
onClick: () => navigate(-1),
}),
getRightNavProps: () => ({
onClick: () => navigate(1),
}),
isTouchDevice,
navigate,
scrollAreaRef: scrollArea,
scrollPosition,
showNav,
};
}
@mryechkin
Copy link

Hey! Do you have a working example of how to use this by chance? I wanted to give it a try but not quite sure where scrollAreaRef needs to go. And how do I structure the children? Appreciate any info you can provide.

@benknight
Copy link
Author

benknight commented Aug 16, 2023

Hey @mryechkin, how did you happen to stumble upon this Gist?

Sure, if you want to try using it, there's a working example in this repo: https://github.com/benknight/vietnamcoracle-next

It's powering the carousels on https://www.vietnamcoracle.com/ (not the main slider but the sections below with headings)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment