How to Add Stick-to-Bottom Features to Shadcn ScrollArea for Better Chat Interfaces
Improving Chat User Experience with Uniform shadcn Design

I am a self taught programmer and love technology in every form and medium, learning new things every day, and constantly tackling new challenges.
When building modern chat applications, one of the most crucial UX patterns is ensuring the scroll area automatically sticks to the bottom as new messages arrive. Users expect to see the latest messages without manually scrolling down every time. While AI SDK's conversation components provide this functionality out of the box, they come with their own styling that doesn't always align with your design system.
The Challenge
I was working on a chat interface using shadcn/ui components to maintain design consistency across my application. The shadcn ScrollArea component provides beautiful, theme-aware scrollbars that integrate seamlessly with the overall design system. However, it lacks the stick-to-bottom functionality that's essential for chat interfaces.
The AI SDK conversation components do include this behaviour, but they come with stock scrollbars that don't match the polished aesthetic of shadcn components. This created a dilemma: sacrifice design consistency for functionality, or lose essential UX behavior for visual appeal.
The Solution: Hybrid Approach
Rather than choosing between design and functionality, I decided to extend the existing shadcn ScrollArea component to support both modes of operation:
Default mode: Standard scrolling behaviour for general content
Stick-to-bottom mode: Automatic bottom-sticking for chat interfaces
This approach preserves the beautiful shadcn styling while adding the sophisticated scroll management needed for conversational UIs.
Implementation Strategy
1. Context-Based State Management
The first step was creating a React context to share scroll state across components:
interface ScrollAreaContextType {
isAtBottom: boolean;
isNearBottom: boolean;
scrollToBottom: () => void;
}
This context provides three key pieces of information:
Current scroll position relative to bottom
Programmatic scroll control
Near-bottom detection for UI optimisations
2. External Library Integration
I leveraged the use-stick-to-bottom library, which handles the complex scroll event management and position calculations. The challenge was integrating this library's refs with Radix UI's ScrollArea primitive:
const mergedScrollRef = useCallback(
(node: HTMLDivElement | null) => {
localContainerRef.current = node;
if (!libScrollRef) return;
if (typeof libScrollRef === "function") libScrollRef(node);
else (libScrollRef as React.RefObject<HTMLDivElement | null>).current = node;
},
[libScrollRef],
);
This ref merging pattern ensures both the external library and our component maintain proper DOM references while avoiding conflicts.
3. Mode-Based Component Switching
The enhanced ScrollArea component now accepts a mode prop that determines behaviour:
function ScrollArea({ mode = "default", ...props }: ScrollAreaProps) {
if (mode === "stick-to-bottom") {
return <ScrollAreaBottomStick {...props} />;
}
return <StandardScrollArea {...props} />;
}
This preserves backward compatibility while adding new functionality.
4. Smart Scroll Button
The ScrollButton component demonstrates context usage by conditionally rendering based on scroll position:
function ScrollButton() {
const { isAtBottom, scrollToBottom } = useScrollArea();
return (
!isAtBottom && (
<Button onClick={scrollToBottom}>
<ArrowDownIcon />
</Button>
)
);
}
This provides users with an intuitive way to jump back to the latest messages when they've scrolled up to read history.
Key Technical Decisions
Ref Management Strategy
One of the trickiest aspects was properly forwarding refs between multiple layers:
Radix UI ScrollArea needs refs for scroll event handling
The stick-to-bottom library needs refs for position calculations
Our component might need refs for additional functionality
The solution was a ref merging pattern that accommodates both function refs and object refs, ensuring compatibility across different ref patterns.
Performance Optimisation
The implementation includes several performance optimisations:
const values = useMemo(() => {
return {
isNearBottom,
isAtBottom,
scrollToBottom,
};
}, [isAtBottom, scrollToBottom, isNearBottom]);
Context values are memoized to prevent unnecessary re-renders of child components, and scroll event handlers are optimised within the external library.
Error Boundaries
The useScrollArea hook includes proper error handling to prevent runtime errors when used incorrectly:
if (!context) {
throw new Error(
"useScrollArea must be used within a <ScrollArea mode='stick-to-bottom'>",
);
}
This provides clear developer feedback and prevents silent failures.
Usage in Practice
The enhanced component maintains the same API as the original shadcn ScrollArea while adding new capabilities:
// Standard scrolling (unchanged behavior)
<ScrollArea className="h-96">
<div>Regular content</div>
</ScrollArea>
// Chat interface with stick-to-bottom
<ScrollArea mode="stick-to-bottom" className="h-96">
<div className="space-y-2">
{messages.map(msg => <Message key={msg.id} {...msg} />)}
</div>
<ScrollButton />
</ScrollArea>
Results and Benefits
This solution delivers several key advantages:
Design Consistency: Maintains shadcn's beautiful scrollbar styling and theme integration across the entire application.
Enhanced UX: Provides smooth stick-to-bottom behaviour that users expect from modern chat interfaces.
Developer Experience: Offers a simple, intuitive API that doesn't break existing code while adding powerful new functionality.
Flexibility: The context system allows for custom scroll controls and behaviours beyond the basic ScrollButton.
Performance: Optimised ref handling and memoization ensure smooth performance even with frequent content updates.
Conclusion
Building great user interfaces often requires bridging the gap between design systems and specialised functionality. By extending shadcn's ScrollArea component rather than replacing it, we preserved the design consistency that makes shadcn valuable while adding the behavioural sophistication needed for modern chat interfaces.
This pattern—extending existing design system components with new modes—is broadly applicable. It allows teams to maintain design consistency while adapting components for specialised use cases, creating a more cohesive and polished user experience.
The result is a chat interface that looks native to your design system while providing the smooth, intuitive scrolling behaviour users expect from professional applications.



