@probably-not/stick-to-bottom
v1.0.5
Published
A lightweight vanilla JavaScript library that automatically sticks to the bottom of a container and smoothly animates content while new items are added
Maintainers
Readme
stick-to-bottom
DISCLAIMER: THIS LIBRARY WAS 100% VIBE CODED BY CLAUDE CODE SONNET.
Vibe-coding methodology can be found here
A lightweight zero-dependency vanilla JavaScript library that automatically sticks to the bottom of a container and smoothly animates content while new items are added. Perfect for chat applications, live logs, and any streaming content interface.
✨ Features
- Framework Agnostic: Pure vanilla JavaScript, works with any framework or no framework
- Zero Dependencies: No external dependencies, lightweight and fast
- Smooth Animations: Velocity-based spring animations with configurable parameters
- Smart Scroll Detection: Distinguishes between user scrolling and programmatic scrolling
- Resize Handling: Automatically handles content size changes using ResizeObserver
- Mobile Friendly: Works seamlessly on touch devices
- TypeScript Support: Full TypeScript definitions included
- Escape Detection: Users can scroll up to "escape" the sticky behavior
- Selection Aware: Pauses scrolling when user is selecting text
🚀 Installation
npm install @probably-not/stick-to-bottomyarn add @probably-not/stick-to-bottompnpm add @probably-not/stick-to-bottom📖 Usage
Basic Usage
import StickToBottom from '@probably-not/stick-to-bottom';
// Get your container and content elements
const container = document.querySelector('.chat-container');
const content = document.querySelector('.messages');
// Create the stick-to-bottom instance
const stickToBottom = new StickToBottom(container, content);
// Add new messages
const newMessage = document.createElement('div');
newMessage.textContent = 'Hello world!';
content.appendChild(newMessage);
// The container will automatically scroll to bottom with smooth animationWith Options
const stickToBottom = new StickToBottom(container, content, {
// Spring animation configuration
damping: 0.7, // How much to damp the animation (0-1)
stiffness: 0.05, // Animation stiffness
mass: 1.25, // Animation mass
// Behavior options
initial: 'smooth', // Scroll to bottom on initialization
resize: 'smooth', // How to handle resize events
// Custom target calculation
targetScrollTop: (target, { scrollElement, contentElement }) => {
return target - 50; // Leave 50px from bottom
}
});Event Handling
// Listen for state changes
const unsubscribe = stickToBottom.on('bottomChange', (isAtBottom) => {
const scrollButton = document.querySelector('.scroll-to-bottom');
scrollButton.style.display = isAtBottom ? 'none' : 'block';
});
// Listen for escape events
stickToBottom.on('escapeChange', (hasEscaped) => {
console.log('User has escaped sticky behavior:', hasEscaped);
});
// Listen for all state changes
stickToBottom.on('stateChange', (state) => {
console.log('State changed:', state);
});
// Clean up
unsubscribe();Programmatic Scrolling
// Scroll to bottom with default animation
stickToBottom.scrollToBottom();
// Scroll with custom animation
stickToBottom.scrollToBottom({
animation: 'instant', // or spring config object
duration: 500, // wait 500ms before allowing completion
ignoreEscapes: true // ignore user scroll during animation
});
// Scroll with promise handling
stickToBottom.scrollToBottom().then((success) => {
if (success) {
console.log('Scrolled to bottom successfully');
} else {
console.log('Scroll was cancelled');
}
});📚 API Reference
Constructor
new StickToBottom(scrollElement, contentElement, options)Parameters:
scrollElement(HTMLElement): The scrollable containercontentElement(HTMLElement): The content element to observeoptions(Object, optional): Configuration options
Options:
damping(number, default: 0.7): Animation damping (0-1)stiffness(number, default: 0.05): Animation stiffnessmass(number, default: 1.25): Animation massinitial(boolean|string|Object, default: true): Initial scroll behaviorresize(string|Object, default: inherited): Resize scroll behaviortargetScrollTop(function): Custom target scroll calculation
Methods
scrollToBottom(options)
Scrolls to the bottom with optional configuration.
Parameters:
options.animation(string|Object): Animation configuration ('instant' or spring config)options.duration(number|Promise): Duration to wait before completionoptions.wait(number|boolean): Wait time before startingoptions.preserveScrollPosition(boolean): Don't change isAtBottom stateoptions.ignoreEscapes(boolean): Ignore user scroll during animation
Returns: Promise - Resolves to true if successful
on(event, handler)
Subscribe to events.
Events:
bottomChange: Fired when isAtBottom state changesescapeChange: Fired when user escapes sticky behaviornearBottomChange: Fired when near bottom state changesstateChange: Fired on any state change
Returns: Function to unsubscribe
off(event, handler)
Unsubscribe from events.
setScrollElement(element)
Change the scroll element.
setContentElement(element)
Change the content element.
destroy()
Clean up all event listeners and resources.
Properties
state
Current state object containing:
isAtBottom(boolean): Whether currently at bottomescapedFromLock(boolean): Whether user has scrolled upisNearBottom(boolean): Whether near bottom (within offset)scrollTop(number): Current scroll positiontargetScrollTop(number): Target scroll position
scrollTop (getter/setter)
Get or set the current scroll position.
isNearBottom (getter)
Check if scroll position is near bottom.
🔧 Configuration
Animation Configuration
{
damping: 0.7, // How much to slow down the animation (0 = no damping, 1 = full damping)
stiffness: 0.05, // How quickly animation reaches target (higher = faster)
mass: 1.25 // Inertial mass (higher = slower, more momentum)
}Behavior Options
{
initial: 'smooth', // Scroll on initialization: true, false, 'instant', 'smooth', or animation config
resize: 'smooth', // Scroll on resize: 'instant', 'smooth', or animation config
targetScrollTop: (target, elements) => {
// Custom target calculation
return target - 100; // Leave 100px from bottom
}
}🌟 Advanced Usage
Custom Animation Presets
const animations = {
bounce: { damping: 0.5, stiffness: 0.1, mass: 0.8 },
gentle: { damping: 0.8, stiffness: 0.03, mass: 1.5 },
snappy: { damping: 0.6, stiffness: 0.08, mass: 1.0 }
};
const stickToBottom = new StickToBottom(container, content, {
...animations.gentle,
resize: animations.snappy
});Dynamic Element Management
class DynamicChat {
constructor() {
this.stickToBottom = new StickToBottom(null, null);
this.setupDynamicElements();
}
setupDynamicElements() {
// Change containers dynamically
document.addEventListener('tab-change', (e) => {
const newContainer = document.querySelector(`#${e.detail.tabId} .chat`);
const newContent = newContainer.querySelector('.messages');
this.stickToBottom.setScrollElement(newContainer);
this.stickToBottom.setContentElement(newContent);
});
}
}🤝 Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📝 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Inspired by the React
useStickToBottomhook from Stackblitz Labs - Built for modern web applications that need smooth scrolling behavior
- Designed with chat applications and live content in mind
