Skip to content

Instantly share code, notes, and snippets.

@matt-morris
Forked from benknight/README.md
Created June 18, 2021 16:01
Show Gist options
  • Save matt-morris/eb88cace28617cc03decb86cc85c6ecf to your computer and use it in GitHub Desktop.
Save matt-morris/eb88cace28617cc03decb86cc85c6ecf 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