{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-scroll-fade", "type": "registry:hook", "title": "useScrollFade", "description": "Vertical edge-fade masks for a height-capped scrollable panel; returns the CSS class to apply.", "categories": [ "hooks" ], "files": [ { "path": "hooks/use-scroll-fade.ts", "type": "registry:hook", "target": "hooks/use-scroll-fade.ts", "content": "import { useEffect, useState, type RefObject } from 'react'\n\nexport type ScrollFadeClass = '' | 'scroll-fade-top' | 'scroll-fade-bottom' | 'scroll-fade-both'\n\n/**\n * Vertical counterpart of `useTabBarFade`: tracks whether the given scroll\n * container has overflow content above, below, both, or neither, and returns\n * the CSS mask class to apply (`scroll-fade-top` / `scroll-fade-bottom` /\n * `scroll-fade-both` from the DS utilities). Apply the class to the scroll\n * container itself -- the overflowing edge fades out, and once scrolled with\n * content on both sides, both edges fade.\n *\n * Listens to scroll on the container, plus resize (window + ResizeObserver on\n * the container and its children) and child add/remove, so the class updates\n * when the container size or its content changes.\n *\n * `changeKey` is an optional value that, when it changes, forces the effect to\n * re-run. Use this when the scroll container is conditionally rendered or only\n * becomes scrollable on a state change (e.g. an expandable grid): pass that\n * state so the hook re-attaches once the ref points at the scrollable element.\n */\nexport function useScrollFade(ref: RefObject, changeKey?: unknown): ScrollFadeClass {\n const [fade, setFade] = useState('')\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const update = () => {\n const { scrollTop, scrollHeight, clientHeight } = el\n // 1px tolerance handles sub-pixel rendering at the extreme edges.\n const canUp = scrollTop > 1\n const canDown = scrollTop < scrollHeight - clientHeight - 1\n const cls: ScrollFadeClass =\n canUp && canDown ? 'scroll-fade-both'\n : canUp ? 'scroll-fade-top'\n : canDown ? 'scroll-fade-bottom'\n : ''\n setFade(prev => prev === cls ? prev : cls)\n }\n\n update()\n el.addEventListener('scroll', update, { passive: true })\n const ro = new ResizeObserver(update)\n ro.observe(el)\n // Also observe each child -- a scrollable container's own size doesn't\n // change when overflow children are added, but ResizeObserver on the\n // children catches their sizing (e.g., async-loaded rows).\n Array.from(el.children).forEach(child => ro.observe(child))\n // MutationObserver catches add/remove of rows (e.g., when the data\n // snapshot arrives and renders the list).\n const mo = new MutationObserver(() => {\n Array.from(el.children).forEach(child => ro.observe(child))\n update()\n })\n mo.observe(el, { childList: true })\n window.addEventListener('resize', update)\n\n return () => {\n el.removeEventListener('scroll', update)\n ro.disconnect()\n mo.disconnect()\n window.removeEventListener('resize', update)\n }\n }, [ref, changeKey])\n\n return fade\n}\n" } ], "docs": "The vertical counterpart to useTabBarFade, for reorder lists and icon grids. Apply the returned class to the scroll container. ReorderList integrates it already. Applies at every viewport size.", "meta": { "group": "hooks", "related": [ "use-tab-bar-fade", "reorder-list" ], "exports": [ "useScrollFade" ], "siteSlug": "use-scroll-fade" } }