A powerful Svelte 5 hook for automatically scrolling to the bottom of containers when new content is added, with intelligent user interaction handling and configurable spring animations.
Perfect for chat interfaces, live logs, streaming content, and any scenario where you want to keep users at the bottom of scrollable content while respecting their reading preferences.
(aside from all the other features from the original package...)
pnpm install stick-to-bottom-svelte
<script lang="ts">
import { StickToBottom } from 'stick-to-bottom-svelte';
let messages = $state([]);
let scrollElement = $state<HTMLElement>();
let contentElement = $state<HTMLElement>();
const stickToBottom = new StickToBottom({
scrollElement: () => scrollElement,
contentElement: () => contentElement
});
// Add new messages - they'll automatically scroll to bottom
function addMessage(text: string) {
messages = [...messages, { text, id: Date.now() }];
}
</script>
<div bind:this={scrollElement} class="h-96 overflow-y-auto">
<div bind:this={contentElement}>
{#each messages as message (message.id)}
<div>{message.text}</div>
{/each}
</div>
</div>
Customize the scroll animation with spring physics:
<script>
const stickToBottom = new StickToBottom({
scrollElement: () => scrollElement,
contentElement: () => contentElement,
// Spring animation configuration
damping: 0.8, // 0-1, higher = less oscillation
stiffness: 0.1, // 0-1, higher = faster animation
mass: 1.0, // 0.5-3, higher = slower animation
// Animation for resize events
resize: 'instant', // or spring config object
// Initial scroll behavior
initial: true // false to disable initial scroll
});
</script>
Control exactly where the "bottom" should be:
<script>
const stickToBottom = new StickToBottom({
scrollElement: () => scrollElement,
contentElement: () => contentElement,
// Custom target calculation
targetScrollTop: (defaultTarget, { scrollElement, contentElement }) => {
// Scroll to 50px before the actual bottom
return defaultTarget - 50;
}
});
</script>
interface StickToBottomOptions {
// Required: Element references
scrollElement: () => HTMLElement | null | undefined;
contentElement: () => HTMLElement | null | undefined;
// Spring animation configuration
damping?: number; // Default: 0.7
stiffness?: number; // Default: 0.05
mass?: number; // Default: 1.25
// Behavior configuration
resize?: Animation; // Animation for resize events
initial?: Animation | boolean; // Initial scroll behavior
targetScrollTop?: GetTargetScrollTop; // Custom target calculation
}
// State properties (reactive with Svelte 5 runes)
stickToBottom.isNearBottom; // boolean - true when near bottom
stickToBottom.scrollTop; // number - current scroll position
stickToBottom.targetScrollTop; // number - target scroll position
// Scroll to bottom with options
stickToBottom.scrollToBottom(options?: ScrollToBottomOptions): Promise<boolean>
// Stop auto-scrolling (user has "escaped")
stickToBottom.stopScroll(): void
// Check if user is currently selecting text
stickToBottom.isSelecting(): boolean
type ScrollToBottomOptions =
| 'instant' // Immediate scroll
| {
animation?: 'instant' | SpringAnimation;
wait?: boolean | number; // Wait for existing scroll
ignoreEscapes?: boolean; // Prevent user interruption
preserveScrollPosition?: boolean; // Only scroll if already at bottom
duration?: number | Promise<void>; // Additional duration to maintain
};
// Spring animation configuration
interface SpringAnimation {
damping?: number; // 0-1, controls oscillation
stiffness?: number; // 0-1, controls speed
mass?: number; // 0.5-3, controls momentum
}
// Animation can be instant or spring-based
type Animation = 'instant' | SpringAnimation;
<script lang="ts">
import { StickToBottom } from 'stick-to-bottom-svelte';
let messages = $state([]);
let newMessage = $state('');
let scrollElement = $state<HTMLElement>();
let contentElement = $state<HTMLElement>();
const stickToBottom = new StickToBottom({
scrollElement: () => scrollElement,
contentElement: () => contentElement,
// Smooth spring animation for chat
damping: 0.7,
stiffness: 0.08,
mass: 1.2
});
function sendMessage() {
if (newMessage.trim()) {
messages = [
...messages,
{
text: newMessage,
timestamp: new Date(),
id: Date.now()
}
];
newMessage = '';
}
}
// Handle incoming messages from WebSocket, etc.
function onIncomingMessage(message) {
messages = [...messages, message];
// Will automatically scroll to bottom if user is already there
}
</script>
<div class="chat-container">
<div bind:this={scrollElement} class="messages-area">
<div bind:this={contentElement}>
{#each messages as message (message.id)}
<div class="message">
{message.text}
<span class="timestamp">{message.timestamp.toLocaleTimeString()}</span>
</div>
{/each}
</div>
</div>
<div class="input-area">
<input
bind:value={newMessage}
onkeydown={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
/>
<button onclick={sendMessage}>Send</button>
</div>
</div>
<script lang="ts">
import { StickToBottom } from 'stick-to-bottom-svelte';
let logs = $state([]);
let scrollElement = $state<HTMLElement>();
let contentElement = $state<HTMLElement>();
const stickToBottom = new StickToBottom({
scrollElement: () => scrollElement,
contentElement: () => contentElement,
// Instant scroll for logs - no animation needed
resize: 'instant',
initial: 'instant'
});
// Simulate log streaming
let logInterval;
onMount(() => {
logInterval = setInterval(() => {
logs = [
...logs,
{
level: 'info',
message: `Log entry at ${new Date().toISOString()}`,
id: Date.now()
}
];
}, 1000);
});
onDestroy(() => {
clearInterval(logInterval);
});
</script>
<div bind:this={scrollElement} class="log-viewer">
<div bind:this={contentElement}>
{#each logs as log (log.id)}
<div class="log-entry {log.level}">
[{log.level.toUpperCase()}] {log.message}
</div>
{/each}
</div>
</div>
<script lang="ts">
// ... setup ...
// Force scroll to bottom
function forceScrollToBottom() {
stickToBottom.scrollToBottom({ animation: 'instant' });
}
// Scroll with custom animation
function smoothScrollToBottom() {
stickToBottom.scrollToBottom({
animation: { damping: 0.9, stiffness: 0.1, mass: 0.8 },
duration: 500 // Keep at bottom for 500ms
});
}
// Stop auto-scrolling
function pauseAutoScroll() {
stickToBottom.stopScroll();
}
// Check scroll state
$effect(() => {
if (stickToBottom.isNearBottom) {
console.log('User is at the bottom - auto-scroll active');
} else {
console.log('User scrolled up - auto-scroll paused');
}
});
</script>
The hook automatically scrolls to bottom when:
scrollToBottom()
is called manuallyAuto-scroll is paused when:
stopScroll()
Auto-scroll resumes when:
scrollToBottom()
call is madeThis package is inspired by the original use-stick-to-bottom package, which is a great library for automatically scrolling to the bottom of containers when new content is added.
This package is a Svelte 5 port of the original package, with all the features of the original package, plus some additional features.
This package is written in TypeScript and includes full type definitions. All interfaces and types are exported for use in your applications.
Contributions are welcome! Please read our contributing guidelines and submit pull requests for any improvements.
MIT License - see LICENSE file for details.