Skip to content

Instantly share code, notes, and snippets.

@ahallora
Last active February 21, 2024 07:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahallora/441f7d7b37688433cf6077458f0240e0 to your computer and use it in GitHub Desktop.
Save ahallora/441f7d7b37688433cf6077458f0240e0 to your computer and use it in GitHub Desktop.
useScrollableMask
/*
HOW TO USE:
import React from "react";
import useScrollableMask from "./useScrollableMask";
const Example = () => {
const { scrollDiv, scrollDivStyles } = useScrollableMask();
return (
<div
style={{
maxHeight: "50svh",
...scrollDivStyles,
}}
ref={scrollDiv}
>
<div
style={{
height: 3000,
width: "100%",
background: "linear-gradient(0deg, red, blue)",
}}
/>
</div>
);
};
export default Example;
*/
import { useCallback, useEffect, useRef, useState } from "react";
/**
* A helper function that adds masking gradients for scrollable elements
* to increase visual guidance on scrollability on both desktop and mobile.
* @param maxHeight max height before applying scrolling in pixels
* @param gradientSize the gradient height in pixels
* @returns - `scrollDiv`: the react reference to be used on the scrolling element
* - `scrollDivStyles`: the default styles to be applied to the scrolling element
*/
const useScrollableMask = (gradientSize = 48) => {
const MASK = {
top: `linear-gradient(#0000001a, #000 ${gradientSize}px)`,
middle: `linear-gradient(#0000001a, #000 ${gradientSize}px, #000 calc(100% - ${gradientSize}px), #0000001a)`,
bottom: `linear-gradient(#000, #000 calc(100% - ${gradientSize}px), #0000001a)`,
};
const [isHovered, setIsHovered] = useState(false);
const [canScroll, setCanScroll] = useState(false);
const scrollDivRef = useRef<HTMLDivElement | null>(null);
const initScrollDivRef = useCallback(node => {
if (!node) return;
scrollDivRef.current = node;
setCanScroll(node.scrollHeight > node.clientHeight);
}, []);
const scrollDivStyles = canScroll
? {
position: "relative",
maskImage: MASK.bottom,
}
: {};
const getActiveMask = (target: HTMLElement) => {
if (!target) return MASK.bottom;
const { scrollHeight, scrollTop, clientHeight } = target;
const isAtBottom = scrollHeight - scrollTop === clientHeight;
const isAtTop = scrollTop === 0;
if (isAtBottom) return MASK.top;
if (isAtTop) return MASK.bottom;
return MASK.middle;
};
useEffect(() => {
if (!scrollDivRef.current || !canScroll) return undefined;
const handleHover = (event: Event) => {
const target = event.target as HTMLElement;
const mouseover = event.type === "mouseenter";
target.style.maskImage = mouseover ? "unset" : getActiveMask(target);
setIsHovered(mouseover);
};
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
if (isHovered) return;
target.style.maskImage = getActiveMask(target);
};
scrollDivRef.current.addEventListener("mouseenter", handleHover);
scrollDivRef.current.addEventListener("mouseleave", handleHover);
scrollDivRef.current.addEventListener("scroll", handleScroll);
return () => {
scrollDivRef.current?.removeEventListener("mouseenter", handleHover);
scrollDivRef.current?.removeEventListener("mouseleave", handleHover);
scrollDivRef.current?.removeEventListener("scroll", handleScroll);
};
}, [isHovered, setIsHovered, scrollDivRef, canScroll]);
return { scrollDiv: initScrollDivRef, scrollDivStyles };
};
export default useScrollableMask;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment