Skip to content

Instantly share code, notes, and snippets.

@bluebeel
Forked from benknight/README.md
Created October 13, 2021 15:46
Show Gist options
  • Save bluebeel/f3fb98c53fd866b9cb49c0f30e960845 to your computer and use it in GitHub Desktop.
Save bluebeel/f3fb98c53fd866b9cb49c0f30e960845 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,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment