scroll-to-future
Scroll To Future
A customizable React scrollbar component with vertical and horizontal scrolling, draggable thumbs, automatic size calculation, mobile fallback, native scrollbar hiding, and built-in themes.
Features
- Vertical and horizontal scrollbars
- Automatic thumb sizing
- Drag-to-scroll support
- Clickable scrollbar track
- Support for regular elements and page scrolling
- Automatic target detection
- Automatic content and container observation
- Native scrollbar hiding
- Native scrolling fallback on mobile devices
- Overlay and reserved-space positioning
- Built-in theme presets
- Fully customizable scrollbar and thumb styles
- TypeScript support
- React and Next.js compatible
20+ Themes in box
- "primary"
- "midnight"
- "neonCyan"
- "ocean"
- "deepSea"
- "forest"
- "moss"
- "lava"
- "ember"
- "gold"
- "roseQuartz"
- "violet"
- "royal"
- "arctic"
- "glass"
- "graphite"
- "terminal"
- "toxic"
- "candy"
- "sand"
- "monoLight"
- "monoDark";
Installation
npm install scroll-to-future
yarn add scroll-to-future
pnpm add scroll-to-future
Basic usage
Improt styles
import 'scroll-to-future/style.css';
The scroll container must have a constrained size and an overflow value such as auto or scroll.
import { ScrollToFuture } from "scroll-to-future";
export const Example = () => {
return (
<div
style={{
position: "relative",
width: "400px",
height: "300px",
overflow: "auto",
}}
>
<ScrollToFuture />
<div style={{ minHeight: "1000px" }}>
Scrollable content
</div>
</div>
);
};
When target is not provided, ScrollToFuture automatically uses its parent element as the scroll target.
For automatic target detection, place the component directly inside the scrollable container.
Usage with a target ref
Use the target property when the scrollbar is rendered outside the scrollable element or when explicit target control is required.
import { useRef } from "react";
import { ScrollToFuture } from "scroll-to-future";
export const Example = () => {
const scrollRef = useRef<HTMLDivElement>(null);
return (
<div style={{ position: "relative" }}>
<div
ref={scrollRef}
style={{
width: "400px",
height: "300px",
overflow: "auto",
}}
>
<div style={{ minHeight: "1000px" }}>
Scrollable content
</div>
</div>
<ScrollToFuture target={scrollRef} />
</div>
);
};
Vertical scrollbar
<ScrollToFuture
scrollBar={{
mode: "vertical",
}}
/>
Horizontal scrollbar
<div
style={{
position: "relative",
width: "600px",
overflowX: "auto",
overflowY: "hidden",
}}
>
<ScrollToFuture
scrollBar={{
mode: "horizontal",
}}
/>
<div style={{ width: "1600px", whiteSpace: "nowrap" }}>
Horizontally scrollable content
</div>
</div>
Vertical and horizontal scrollbars
<ScrollToFuture
scrollBar={{
mode: "both",
}}
/>
Configuration
import type { ScrollToFutureConfig } from "scroll-to-future";
import "scroll-to-future/styles.css";
const config: ScrollToFutureConfig = {
scrollBar: {
mode: "both",
positionMode: "after",
superimposition: "after",
boundaryOffset: "4px",
widthTrack: "8px",
heightTrack: "98%",
hideNativeScrollbar: "always",
},
thumb: {
boundaryOffset: "1px 1px",
heightTrack: "auto",
},
nativeOnMobile: true,
selectTheme: "primary",
optionsTheme: {},
};
<ScrollToFuture {...config} />
Component properties
| Property | Type | Default | Description |
|---|---|---|---|
target |
React.RefObject<HTMLElement | null> |
Parent element | Element whose scrolling is controlled |
scrollBar |
ScrollToFutureScrollBar |
Default scrollbar config | Track and positioning configuration |
thumb |
ScrollToFutureThumb |
Default thumb config | Thumb size and offset configuration |
selectTheme |
PresetsThemeType |
"primary" |
Built-in theme name |
optionsTheme |
ScrollToFutureThemeProps |
{} |
Custom theme overrides |
nativeOnMobile |
boolean |
true |
Use native scrolling on mobile-only input devices |
Scrollbar configuration
mode
Controls which scrollbars are rendered.
type ScrollBarMode = "horizontal" | "vertical" | "both";
<ScrollToFuture
scrollBar={{
mode: "vertical",
}}
/>
Default:
"both"
positionMode
Controls the side on which the custom scrollbar is rendered.
type PositionMode = "before" | "after";
For a vertical scrollbar:
"before"places the track on the left."after"places the track on the right.
For a horizontal scrollbar:
"before"places the track at the top."after"places the track at the bottom.
<ScrollToFuture
scrollBar={{
positionMode: "before",
}}
/>
Default:
"after"
superimposition
Controls whether the scrollbar overlaps the content or reserves space for itself.
type Superimposition = "over" | "after";
Overlay mode
The scrollbar is placed over the content.
<ScrollToFuture
scrollBar={{
superimposition: "over",
}}
/>
Reserved-space mode
The component adds padding to the target element so the scrollbar does not cover its content.
<ScrollToFuture
scrollBar={{
superimposition: "after",
}}
/>
Default:
"after"
boundaryOffset
Sets the outer offset of the track.
Accepted values:
type BoundaryOffset =
| `${number}px`
| `${number}px ${number}px`;
A single value applies the same offset to both sides:
<ScrollToFuture
scrollBar={{
boundaryOffset: "6px",
}}
/>
Two values define the start and end offsets:
<ScrollToFuture
scrollBar={{
boundaryOffset: "4px 8px",
}}
/>
Default:
"4px"
widthTrack
Sets the scrollbar track thickness.
<ScrollToFuture
scrollBar={{
widthTrack: "10px",
}}
/>
Accepted value:
`${number}px`
The effective default thickness is 8px.
heightTrack
Sets the length of the scrollbar track.
Accepted values:
type HeightTrackType =
| `${number}px`
| `${number}%`
| `${number}vh`
| `${number}dvh`
| `${number}dsvh`;
Examples:
<ScrollToFuture
scrollBar={{
heightTrack: "80%",
}}
/>
<ScrollToFuture
scrollBar={{
heightTrack: "240px",
}}
/>
<ScrollToFuture
scrollBar={{
heightTrack: "80dvh",
}}
/>
Default:
"98%"
hideNativeScrollbar
Controls when the browser's native scrollbar is hidden.
type HideNativeScrollbarMode =
| false
| "fine-pointer"
| "always";
Do not hide the native scrollbar
<ScrollToFuture
scrollBar={{
hideNativeScrollbar: false,
}}
/>
Hide only when a fine pointer is available
This normally applies to devices with a mouse, trackpad, stylus, or another precise pointer.
<ScrollToFuture
scrollBar={{
hideNativeScrollbar: "fine-pointer",
}}
/>
Always hide the native scrollbar
<ScrollToFuture
scrollBar={{
hideNativeScrollbar: "always",
}}
/>
Default:
"always"
The native scrollbar is hidden only when the custom component covers every currently scrollable axis.
For example, when the target can scroll both horizontally and vertically but mode is set to "vertical", the native scrollbar is preserved because the horizontal axis is not covered by the custom scrollbar.
Thumb configuration
boundaryOffset
Controls the space between the thumb and the track boundaries.
<ScrollToFuture
thumb={{
boundaryOffset: "2px",
}}
/>
<ScrollToFuture
thumb={{
boundaryOffset: "2px 4px",
}}
/>
Default:
"1px 1px"
heightTrack
Controls the thumb size.
type ThumbHeight =
| "auto"
| `${number}px`
| `${number}%`;
Automatic size
The thumb size is calculated from the ratio between the visible area and the full scrollable content.
<ScrollToFuture
thumb={{
heightTrack: "auto",
}}
/>
Fixed size
<ScrollToFuture
thumb={{
heightTrack: "48px",
}}
/>
Percentage size
<ScrollToFuture
thumb={{
heightTrack: "20%",
}}
/>
Default:
"auto"
The calculated thumb size is constrained to prevent it from becoming too small or occupying the entire track.
Mobile behavior
By default, mobile-only input devices use the browser's native scrolling behavior.
<ScrollToFuture nativeOnMobile />
This is equivalent to:
<ScrollToFuture nativeOnMobile={true} />
On a device whose primary input is coarse and which has no fine pointer:
- The custom scrollbar is not rendered.
- The browser's native scrollbar is not hidden.
- Native touch scrolling remains available.
To force the custom scrollbar to render on mobile devices:
<ScrollToFuture nativeOnMobile={false} />
Built-in themes
The package includes the following presets:
type PresetsThemeType =
| "primary"
| "midnight"
| "neonCyan"
| "ocean"
| "deepSea"
| "forest"
| "moss"
| "lava"
| "ember"
| "gold"
| "roseQuartz"
| "violet"
| "royal"
| "arctic"
| "glass"
| "graphite"
| "terminal"
| "toxic"
| "candy"
| "sand"
| "monoLight"
| "monoDark";
Select a theme with selectTheme:
<ScrollToFuture selectTheme="neonCyan" />
<ScrollToFuture selectTheme="forest" />
<ScrollToFuture selectTheme="monoDark" />
Custom theme
Use optionsTheme to override part or all of the selected preset.
<ScrollToFuture
selectTheme="primary"
optionsTheme={{
scrollBar: {
inactive: {
backgroundColor: "rgba(255, 255, 255, 0.08)",
borderRadius: "999px",
},
hover: {
backgroundColor: "rgba(255, 255, 255, 0.16)",
},
active: {
backgroundColor: "rgba(255, 255, 255, 0.24)",
},
},
thumb: {
inactive: {
backgroundColor: "#8b5cf6",
borderRadius: "999px",
},
hover: {
backgroundColor: "#a78bfa",
transform: "scale(1.05)",
},
active: {
backgroundColor: "#ddd6fe",
transform: "scale(1.12)",
},
},
}}
/>
Custom theme values are deeply merged with the selected preset. You only need to provide the properties you want to replace.
Theme types
type ScrollToFutureThemeProps = {
scrollBar?: StatusElementsTheme;
thumb?: StatusElementsTheme;
};
type StatusElementsTheme = {
inactive?: ScrollToFutureGeneralTypes;
hover?: ScrollToFutureGeneralTypes;
active?: ScrollToFutureGeneralTypes;
};
type ScrollToFutureGeneralTypes = {
backgroundColor?: string;
opacity?: number;
border?: string;
borderRadius?: string;
outline?: string;
boxShadow?: string;
transition?: string;
transform?: string;
};
Complete customization example
import { useRef } from "react";
import { ScrollToFuture } from "scroll-to-future";
export const CustomScrollbarExample = () => {
const targetRef = useRef<HTMLDivElement>(null);
return (
<div style={{ position: "relative" }}>
<div
ref={targetRef}
style={{
width: "600px",
height: "400px",
overflow: "auto",
}}
>
<div
style={{
width: "1200px",
minHeight: "1400px",
padding: "24px",
}}
>
Scrollable content
</div>
</div>
<ScrollToFuture
target={targetRef}
nativeOnMobile={true}
selectTheme="violet"
scrollBar={{
mode: "both",
positionMode: "after",
superimposition: "over",
boundaryOffset: "6px",
widthTrack: "10px",
heightTrack: "90%",
hideNativeScrollbar: "fine-pointer",
}}
thumb={{
boundaryOffset: "2px",
heightTrack: "auto",
}}
optionsTheme={{
thumb: {
active: {
transform: "scale(1.15)",
boxShadow: "0 0 16px rgba(139, 92, 246, 0.8)",
},
},
}}
/>
</div>
);
};
Next.js
The component uses browser APIs and must be rendered inside a Client Component.
"use client";
import { ScrollToFuture } from "scroll-to-future";
export const ScrollContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<div
style={{
position: "relative",
height: "100dvh",
overflow: "auto",
}}
>
<ScrollToFuture />
{children}
</div>
);
};
How it works
The component observes the target element and its content using browser observers.
It automatically recalculates:
- Scrollable width and height
- Current scroll position
- Visible container size
- Track dimensions
- Thumb dimensions
- Thumb position
- Target element position
- Horizontal and vertical overflow availability
The scrollbar is updated when:
- The target is scrolled
- The window is resized
- The visual viewport changes
- The target changes size
- Child elements change size
- Elements are added or removed
- Relevant styles or classes change
Interaction
Dragging the thumb
Press and drag the thumb to change the target's scroll position.
Pointer capture is used during dragging, so the interaction continues even when the pointer leaves the thumb.
Clicking the track
Click an empty part of the track to move the thumb toward that position.
The requested scroll position is automatically clamped to the available scroll range.
Page scrolling
The component supports page scroll targets such as:
document.bodydocument.documentElementdocument.scrollingElement
When the target is part of the document scrolling system, the component resolves the active scroll container and uses the browser viewport for its measurements.
Important CSS requirements
The target element should normally have:
.scroll-container {
position: relative;
overflow: auto;
width: 100%;
height: 400px;
}
A scrollbar cannot appear when the element has no constrained size or when its content does not exceed its visible area.
For vertical scrolling:
.scroll-container {
overflow-y: auto;
}
For horizontal scrolling:
.scroll-container {
overflow-x: auto;
}
For both axes:
.scroll-container {
overflow: auto;
}
Troubleshooting
The custom scrollbar is not visible
Check that:
- The target has a constrained width or height.
- The content is larger than the target.
- The target uses
overflow: autooroverflow: scroll. - The selected
modeincludes the overflowing axis. - The component is rendered in a Client Component.
nativeOnMobileis not disabling the custom scrollbar on the current device.
The browser scrollbar is still visible
Check the following:
hideNativeScrollbaris notfalse.- The custom component covers every scrollable axis.
- The correct element is passed through
target. - The actual scroll container is not a nested child of the supplied target.
nativeOnMobileis not preserving native scrolling on the current device."fine-pointer"is only active when the device has a fine pointer.
To force native scrollbar hiding on desktop and mobile:
<ScrollToFuture
nativeOnMobile={false}
scrollBar={{
hideNativeScrollbar: "always",
}}
/>
The content moves when the scrollbar appears
The default superimposition value is "after", which reserves space by adding padding to the target.
Use overlay mode when the scrollbar should not affect content spacing:
<ScrollToFuture
scrollBar={{
superimposition: "over",
}}
/>
The scrollbar is rendered on the wrong element
Pass an explicit ref:
const targetRef = useRef<HTMLDivElement>(null);
<div ref={targetRef}>...</div>
<ScrollToFuture target={targetRef} />
Browser requirements
The component relies on modern browser APIs:
ResizeObserverMutationObserver- Pointer Events
requestAnimationFramematchMedia- CSS custom properties
For older browsers, additional polyfills may be required.
TypeScript
The package exports the component and its public configuration type:
export { ScrollToFuture } from "scroll-to-future";
export type {
ScrollToFutureConfig,
} from "scroll-to-future";
Example:
import type { ScrollToFutureConfig } from "scroll-to-future";
const scrollbarConfig: ScrollToFutureConfig = {
selectTheme: "graphite",
scrollBar: {
mode: "vertical",
positionMode: "after",
superimposition: "over",
hideNativeScrollbar: "always",
},
thumb: {
heightTrack: "auto",
},
};