An open API service indexing awesome lists of open source software.

https://github.com/probably-not/stick-to-bottom

A lightweight vanilla JavaScript library that automatically sticks to the bottom of a container and smoothly animates content while new items are added
https://github.com/probably-not/stick-to-bottom

Last synced: 5 months ago
JSON representation

A lightweight vanilla JavaScript library that automatically sticks to the bottom of a container and smoothly animates content while new items are added

Awesome Lists containing this project

README

          

# stick-to-bottom

**DISCLAIMER: THIS LIBRARY WAS 100% VIBE CODED BY CLAUDE CODE SONNET.**

**[Vibe-coding methodology can be found here](./vibing/README.md)**

[![npm version](https://img.shields.io/npm/v/@probably-not/stick-to-bottom.svg?style=flat-square)](https://www.npmjs.com/package/@probably-not/stick-to-bottom)
[![npm downloads](https://img.shields.io/npm/dm/@probably-not/stick-to-bottom.svg?style=flat-square)](https://www.npmjs.com/package/@probably-not/stick-to-bottom)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

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

```bash
npm install @probably-not/stick-to-bottom
```

```bash
yarn add @probably-not/stick-to-bottom
```

```bash
pnpm add @probably-not/stick-to-bottom
```

## 📖 Usage

### Basic Usage

```javascript
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 animation
```

### With Options

```javascript
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

```javascript
// 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

```javascript
// 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

```javascript
new StickToBottom(scrollElement, contentElement, options)
```

**Parameters:**
- `scrollElement` (HTMLElement): The scrollable container
- `contentElement` (HTMLElement): The content element to observe
- `options` (Object, optional): Configuration options

**Options:**
- `damping` (number, default: 0.7): Animation damping (0-1)
- `stiffness` (number, default: 0.05): Animation stiffness
- `mass` (number, default: 1.25): Animation mass
- `initial` (boolean|string|Object, default: true): Initial scroll behavior
- `resize` (string|Object, default: inherited): Resize scroll behavior
- `targetScrollTop` (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 completion
- `options.wait` (number|boolean): Wait time before starting
- `options.preserveScrollPosition` (boolean): Don't change isAtBottom state
- `options.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 changes
- `escapeChange`: Fired when user escapes sticky behavior
- `nearBottomChange`: Fired when near bottom state changes
- `stateChange`: 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 bottom
- `escapedFromLock` (boolean): Whether user has scrolled up
- `isNearBottom` (boolean): Whether near bottom (within offset)
- `scrollTop` (number): Current scroll position
- `targetScrollTop` (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

```javascript
{
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

```javascript
{
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

```javascript
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

```javascript
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

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## 📝 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 🙏 Acknowledgments

- Inspired by the React `useStickToBottom` hook from Stackblitz Labs
- Built for modern web applications that need smooth scrolling behavior
- Designed with chat applications and live content in mind