Designed with AI chat bots in mind, unofficial Vue port of use-stick-to-bottom.
A lightweight Vue 3 component and composable that automatically sticks to the bottom of a scrollable container and smoothly animates content to keep its visual position on screen while new content is being added.
- Does not require
overflow-anchorbrowser-level CSS support (which Safari does not support). - Can be connected to any existing component using a composable with refs, or simply use the provided component which handles refs for you plus provides context via
provide/inject. - Uses the modern, yet well-supported,
ResizeObserverAPI to detect when content resizes.- Supports content shrinking without losing stickiness — not just getting taller.
- Correctly handles Scroll Anchoring. When content above the viewport resizes, the content currently displayed in the viewport won't jump up or down.
- Allows the user to cancel the stickiness at any time by scrolling up.
- Clever logic distinguishes user scrolling from custom animation scroll events (without debouncing which could cause some events to be missed).
- Works well on mobile devices too.
- Uses a custom smooth scrolling algorithm with velocity-based spring animations (configurable parameters).
- Other libraries use easing functions with durations, which don't work well when streaming in new content with variable sizing — common for AI chatbot use cases.
scrollToBottomreturns aPromise<boolean>which resolves totrueonce the scroll succeeds, orfalseif the scroll was cancelled.
- Zero runtime dependencies.
- Tree-shakable ESM build with full TypeScript declarations.
pnpm add vue-stick-to-bottom
# or: npm install / yarn add<script setup lang="ts">
import { StickToBottom, useStickToBottomContext } from 'vue-stick-to-bottom'
</script>
<template>
<StickToBottom class="h-[50vh] relative" resize="smooth" :initial="{ mass: 10 }">
<div class="flex flex-col gap-4">
<Message v-for="msg in messages" :key="msg.id" :message="msg" />
</div>
<template #overlay>
<ScrollToBottom />
</template>
<template #after>
<ChatBox />
</template>
</StickToBottom>
</template>The ScrollToBottom child component can use useStickToBottomContext to access state and actions:
<script setup lang="ts">
import { useStickToBottomContext } from 'vue-stick-to-bottom'
const { isAtBottom, scrollToBottom } = useStickToBottomContext()
</script>
<template>
<button
v-if="!isAtBottom"
class="absolute bottom-2 left-1/2 -translate-x-1/2"
@click="scrollToBottom()"
>
↓ Scroll to bottom
</button>
</template>For full control over the DOM structure, use the composable directly:
<script setup lang="ts">
import { useStickToBottom } from 'vue-stick-to-bottom'
const { scrollRef, contentRef, isAtBottom, scrollToBottom } = useStickToBottom()
</script>
<template>
<div ref="scrollRef" style="overflow: auto; height: 50vh;">
<div ref="contentRef">
<Message v-for="msg in messages" :key="msg.id" :message="msg" />
</div>
</div>
<button v-if="!isAtBottom" @click="scrollToBottom()">↓</button>
</template>All slots (default, overlay, after) expose the following props for convenience:
<StickToBottom>
<template #default="{ isAtBottom, scrollToBottom }">
<!-- Use slot props directly without useStickToBottomContext -->
</template>
</StickToBottom>| Prop | Type | Default | Description |
|---|---|---|---|
resize |
Animation |
Spring | Animation to use when content resizes. |
initial |
Animation | boolean |
Spring | Animation for initial scroll. false to start at top. |
targetScrollTop |
GetTargetScrollTop |
— | Custom function to compute the target scroll position. |
damping |
number |
0.7 |
Spring damping (0–1). 0 = no damping, 1 = full. |
stiffness |
number |
0.05 |
Spring stiffness — how fast the animation accelerates. |
mass |
number |
1.25 |
Spring mass — higher = slower animation. |
anchor |
'auto' | 'none' |
'auto' |
Maps to CSS overflow-anchor. Use 'none' to disable browser scroll anchoring. |
type Animation = ScrollBehavior | SpringAnimation
// ScrollBehavior: 'auto' | 'instant' | 'smooth'
// SpringAnimation: { damping?: number; stiffness?: number; mass?: number }| Slot | Description |
|---|---|
default |
Content inside the scrollable area. |
overlay |
Floats above the scrollable content (e.g. scroll-to-bottom button). Positioned within the relative wrapper. |
after |
Outside the scroll area entirely (e.g. input box, external controls). |
All slots receive { isAtBottom, isNearBottom, escapedFromLock, scrollToBottom, stopScroll } as slot props.
Returns:
| Property | Type | Description |
|---|---|---|
scrollRef |
Ref<HTMLElement | null> |
Template ref for the scroll container. |
contentRef |
Ref<HTMLElement | null> |
Template ref for the content wrapper. |
isAtBottom |
Ref<boolean> |
Whether the container is at (or very near) the bottom. |
isNearBottom |
Ref<boolean> |
Whether the container is within ~70px of the bottom. |
escapedFromLock |
Ref<boolean> |
Whether the user has scrolled up to cancel stickiness. |
scrollToBottom |
ScrollToBottom |
Programmatically scroll to the bottom. |
stopScroll |
StopScroll |
Stop the current scroll animation and release the lock. |
setOptions |
(options) => void |
Update engine options at runtime. |
type ScrollToBottomOptions =
| ScrollBehavior
| {
animation?: Animation
wait?: boolean | number // Wait for existing scrolls or delay in ms
ignoreEscapes?: boolean // Prevent user from cancelling this scroll
preserveScrollPosition?: boolean // Only scroll if already at bottom
duration?: number | Promise<void> // Extra duration to maintain the scroll
}Returns Promise<boolean> — resolves to true if scroll completed, false if cancelled.
Call inside a child component of <StickToBottom> to access the context via inject. Throws if used outside the component tree.
Run the built-in demo:
pnpm devThis opens a playground showcasing smooth vs instant columns, a speed slider, a floating scroll-to-bottom button, and stop-scroll controls.
This is a Vue 3 port of use-stick-to-bottom by Sam Denty / StackBlitz.