A javascript scrollbar plugin which hides native scrollbars, provides custom styleable overlay scrollbars and keeps the native functionality and feeling.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
OverlayScrollbars provides a powerful extension system that allows you to add custom functionality to instances through a plugin architecture. Extensions can be registered globally and used across multiple instances.
Extensions are objects with lifecycle methods that get called when they are added to or removed from an OverlayScrollbars instance. They provide a clean way to extend functionality without modifying the core library.
OverlayScrollbars.extension(
name: string,
extensionConstructor: ExtensionConstructor,
defaultOptions?: object
): boolean;Register an extension globally so it can be used by any instance.
// Register a simple logging extension
OverlayScrollbars.extension('logger', function(instance, options) {
return {
added: function() {
if (options.logInit) {
console.log('Logger extension added to instance');
}
},
removed: function() {
if (options.logDestroy) {
console.log('Logger extension removed from instance');
}
}
};
}, {
// Default extension options
logInit: true,
logDestroy: true,
logScroll: false
});Extensions can be added during initialization or dynamically after creation.
// Add extension during initialization
const instance = OverlayScrollbars(element, {}, {
logger: {
logInit: true,
logScroll: true
}
});
// Add extension after initialization
instance.addExt('logger', {
logInit: false,
logDestroy: true
});interface OverlayScrollbarsInstance {
addExt(extensionName: string, extensionOptions?: object): OverlayScrollbarsExtension | undefined;
removeExt(extensionName: string): boolean;
ext(extensionName: string): OverlayScrollbarsExtension | undefined;
}// Add extension to instance
const extensionInstance = instance.addExt('customExtension', {
option1: 'value1'
});
// Get extension instance
const ext = instance.ext('customExtension');
// Remove extension from instance
const removed = instance.removeExt('customExtension');OverlayScrollbars.extension('autoScroll', function(instance, options) {
let intervalId;
let isScrolling = false;
const startAutoScroll = () => {
if (options.enabled && !isScrolling) {
isScrolling = true;
intervalId = setInterval(() => {
const state = instance.getState();
if (state.destroyed) {
stopAutoScroll();
return;
}
const currentY = state.contentScrollSize.height *
(instance.getElements('viewport').scrollTop /
(state.contentScrollSize.height - state.viewportSize.height));
const newY = currentY + options.speed;
const maxY = state.contentScrollSize.height - state.viewportSize.height;
if (newY >= maxY && options.loop) {
instance.scroll({ y: 0 }, options.resetDuration);
} else if (newY < maxY) {
instance.scroll({ y: newY });
} else {
stopAutoScroll();
}
}, options.interval);
}
};
const stopAutoScroll = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
isScrolling = false;
}
};
return {
added: function() {
if (options.autoStart) {
startAutoScroll();
}
// Add public methods to instance
this.start = startAutoScroll;
this.stop = stopAutoScroll;
this.toggle = () => isScrolling ? stopAutoScroll() : startAutoScroll();
},
removed: function() {
stopAutoScroll();
}
};
}, {
enabled: true,
autoStart: false,
speed: 1,
interval: 50,
loop: true,
resetDuration: 1000
});
// Usage
const instance = OverlayScrollbars(element, {}, {
autoScroll: {
autoStart: true,
speed: 2,
loop: false
}
});
// Control auto-scroll
const autoScrollExt = instance.ext('autoScroll');
autoScrollExt.stop();
autoScrollExt.start();OverlayScrollbars.extension('positionTracker', function(instance, options) {
let positions = [];
let currentIndex = -1;
const savePosition = () => {
const viewport = instance.getElements('viewport');
const position = {
x: viewport.scrollLeft,
y: viewport.scrollTop,
timestamp: Date.now()
};
positions.push(position);
currentIndex = positions.length - 1;
// Limit history size
if (positions.length > options.maxHistory) {
positions = positions.slice(-options.maxHistory);
currentIndex = positions.length - 1;
}
if (options.onPositionSaved) {
options.onPositionSaved(position, positions.length);
}
};
const goToPosition = (index) => {
if (index >= 0 && index < positions.length) {
const position = positions[index];
instance.scroll({ x: position.x, y: position.y }, options.scrollDuration);
currentIndex = index;
return position;
}
return null;
};
return {
added: function() {
// Save initial position
if (options.saveInitial) {
savePosition();
}
// Set up scroll tracking
if (options.trackScroll) {
instance.options('callbacks.onScrollStop', savePosition);
}
// Public API
this.save = savePosition;
this.goTo = goToPosition;
this.getHistory = () => [...positions];
this.clear = () => {
positions = [];
currentIndex = -1;
};
this.back = () => goToPosition(Math.max(0, currentIndex - 1));
this.forward = () => goToPosition(Math.min(positions.length - 1, currentIndex + 1));
},
removed: function() {
// Cleanup if needed
}
};
}, {
maxHistory: 20,
saveInitial: true,
trackScroll: true,
scrollDuration: 300,
onPositionSaved: null
});
// Usage
const instance = OverlayScrollbars(element, {}, {
positionTracker: {
maxHistory: 50,
onPositionSaved: (position, count) => {
console.log(`Position ${count} saved:`, position);
}
}
});
// Use the tracker
const tracker = instance.ext('positionTracker');
tracker.save(); // Manually save current position
tracker.back(); // Go to previous position
tracker.forward(); // Go to next position
console.log(tracker.getHistory()); // Get all saved positionsOverlayScrollbars.extension('syncScroll', function(instance, options) {
let syncGroup = options.group || 'default';
let isUpdating = false;
// Global registry for sync groups
if (!window.OverlayScrollbarsSyncGroups) {
window.OverlayScrollbarsSyncGroups = {};
}
const registry = window.OverlayScrollbarsSyncGroups;
const onScroll = () => {
if (isUpdating) return;
const viewport = instance.getElements('viewport');
const scrollInfo = {
x: viewport.scrollLeft,
y: viewport.scrollTop,
xPercent: viewport.scrollLeft / (viewport.scrollWidth - viewport.clientWidth),
yPercent: viewport.scrollTop / (viewport.scrollHeight - viewport.clientHeight)
};
// Update other instances in the same group
if (registry[syncGroup]) {
registry[syncGroup].forEach(otherInstance => {
if (otherInstance !== instance) {
otherInstance._syncUpdate = true;
if (options.syncMode === 'percent') {
const otherViewport = otherInstance.getElements('viewport');
const targetX = scrollInfo.xPercent * (otherViewport.scrollWidth - otherViewport.clientWidth);
const targetY = scrollInfo.yPercent * (otherViewport.scrollHeight - otherViewport.clientHeight);
otherInstance.scroll({ x: targetX, y: targetY });
} else {
otherInstance.scroll({ x: scrollInfo.x, y: scrollInfo.y });
}
setTimeout(() => {
otherInstance._syncUpdate = false;
}, 10);
}
});
}
};
return {
added: function() {
// Add to sync group
if (!registry[syncGroup]) {
registry[syncGroup] = [];
}
registry[syncGroup].push(instance);
// Set up scroll listener
instance.options('callbacks.onScroll', onScroll);
},
removed: function() {
// Remove from sync group
if (registry[syncGroup]) {
const index = registry[syncGroup].indexOf(instance);
if (index > -1) {
registry[syncGroup].splice(index, 1);
}
// Clean up empty groups
if (registry[syncGroup].length === 0) {
delete registry[syncGroup];
}
}
}
};
}, {
group: 'default',
syncMode: 'percent' // 'percent' or 'absolute'
});
// Usage - synchronize scrolling between multiple elements
const instance1 = OverlayScrollbars(element1, {}, {
syncScroll: { group: 'mainContent' }
});
const instance2 = OverlayScrollbars(element2, {}, {
syncScroll: { group: 'mainContent' }
});
// Now scrolling one will scroll the other// Get all registered extensions
const allExtensions = OverlayScrollbars.extension();
// Get specific extension constructor
const loggerExt = OverlayScrollbars.extension('logger');
// Check if extension exists
if (OverlayScrollbars.extension('customExt')) {
// Extension is registered
}// Unregister an extension
const success = OverlayScrollbars.extension('extensionName', null);interface OverlayScrollbarsExtension {
added(instance: OverlayScrollbarsInstance, options: object): void;
removed?(): void;
}
type ExtensionConstructor = (
instance: OverlayScrollbarsInstance,
options: object
) => OverlayScrollbarsExtension;Inside extension methods, this refers to the extension instance, allowing you to store state and expose public methods.
OverlayScrollbars.extension('statefulExtension', function(instance, options) {
let state = { count: 0 };
return {
added: function() {
// `this` is the extension instance
this.increment = () => state.count++;
this.getCount = () => state.count;
this.reset = () => state.count = 0;
},
removed: function() {
// Cleanup
}
};
});
// Access extension methods
const ext = instance.ext('statefulExtension');
ext.increment();
console.log(ext.getCount()); // 1interface OverlayScrollbarsExtension {
added(instance: OverlayScrollbarsInstance, options: object): void;
removed?(): void;
[key: string]: any; // Extensions can add custom methods
}
type ExtensionConstructor = (
instance: OverlayScrollbarsInstance,
options: object
) => OverlayScrollbarsExtension;
type OverlayScrollbarsExtensions = {
[extensionName: string]: object;
} | object[];Install with Tessl CLI
npx tessl i tessl/npm-overlayscrollbars