Skip to content

Instantly share code, notes, and snippets.

@devhammed
Created October 10, 2025 17:50
Show Gist options
  • Select an option

  • Save devhammed/6c9bf2805d63d9222ae80da2d36ca2af to your computer and use it in GitHub Desktop.

Select an option

Save devhammed/6c9bf2805d63d9222ae80da2d36ca2af to your computer and use it in GitHub Desktop.
Shadcn UI Scroll Area with Overflow Fading Support
import { cn } from '@/lib/utils';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
const ScrollArea = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & { fade?: boolean }
>(({ className, fade, children, ...props }, ref) => {
const viewPortRef = useRef<HTMLDivElement | null>(null);
const [showTopFade, setShowTopFade] = useState(false);
const [showBottomFade, setShowBottomFade] = useState(false);
useEffect(() => {
if (!fade) {
return;
}
const el = viewPortRef.current;
if (!el) {
return;
}
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = el;
setShowTopFade(scrollTop > 0);
setShowBottomFade(scrollTop + clientHeight < scrollHeight);
};
handleScroll();
el.addEventListener('scroll', handleScroll);
return () => {
el.removeEventListener('scroll', handleScroll);
};
}, [fade]);
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
ref={viewPortRef}
className="h-full w-full rounded-[inherit]"
>
{fade && (
<>
<div
data-slot="scroll-area-top-fade"
className={cn(
'pointer-events-none absolute top-0 right-0 left-0 h-6 bg-gradient-to-b from-white to-transparent transition-opacity duration-200',
showTopFade ? 'opacity-100' : 'opacity-0',
)}
/>
<div
data-slot="scroll-area-bottom-fade"
className={cn(
'pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-white to-transparent transition-opacity duration-200',
showBottomFade ? 'opacity-100' : 'opacity-0',
)}
/>
</>
)}
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
});
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
'flex touch-none transition-colors select-none',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment