muazzam/slick-scrollbar
muazzam/slick-scrollbar adds customizable, JavaScript-driven scrollbars to Laravel apps. Swap native browser scrollbars for slim, styled bars using a simple Blade directive or component, with configurable colors and sizes. Lightweight and easy to drop into any Blade view.
muazzam/slick-scrollbar is a frontend asset package, not a backend feature package. It ships CSS/JS for replacing native browser scrollbars with a styled, custom-drawn scrollbar. Treat it the way you'd treat a UI library: install, publish assets, wire into your layout, then progressively apply it to containers that need it.
1. Install
composer require muazzam/slick-scrollbar
2. Publish whatever the provider exposes
Most asset-type Laravel packages register a publishable tag for the config and the CSS/JS files. Check with:
php artisan vendor:publish --provider="Muazzam\\SlickScrollbar\\SlickScrollbarServiceProvider"
# or, if tagged:
php artisan vendor:publish --tag=slick-scrollbar
Inspect vendor/muazzam/slick-scrollbar/ immediately after install — the README, config/, and resources/ (or dist/) folders tell you exactly what's publishable and whether a Blade directive or component is registered. This is the first place to look, because with 13 stars the README is the only reliable documentation you'll get.
3. Include assets in your layout
If the package ships plain assets, reference them from your main layout:
{{-- resources/views/layouts/app.blade.php --}}
@push('styles')
<link rel="stylesheet" href="{{ asset('vendor/slick-scrollbar/slick-scrollbar.css') }}">
@endpush
@push('scripts')
<script src="{{ asset('vendor/slick-scrollbar/slick-scrollbar.js') }}"></script>
@endpush
If you use Vite, prefer importing the published file from your resources/js/app.js / app.css instead, so it's bundled and cache-busted alongside everything else.
4. First use case
Don't apply it globally on day one. Pick a single scrollable region where the native scrollbar is visually wrong — a sidebar nav, a chat/message panel, or a horizontally scrolling data table wrapper. Add the package's required class or data attribute to that container, initialize it, and confirm behavior before rolling it out.
<div class="slick-scrollbar" style="max-height: 400px; overflow-y: auto;">
@foreach ($notifications as $notification)
@include('notifications.item', ['notification' => $notification])
@endforeach
</div>
Centralize initialization in one entrypoint. Rather than inlining init calls in every Blade file, put the bootstrap in resources/js/app.js and have it scan for a known selector or data- attribute:
import './bootstrap';
import SlickScrollbar from './vendor/slick-scrollbar'; // or window.SlickScrollbar
function initScrollbars(root = document) {
root.querySelectorAll('[data-slick-scrollbar]').forEach((el) => {
if (el.dataset.slickInitialized) return; // idempotency guard
new SlickScrollbar(el);
el.dataset.slickInitialized = 'true';
});
}
document.addEventListener('DOMContentLoaded', () => initScrollbars());
window.initScrollbars = initScrollbars; // expose for Livewire/Alpine
Opting in via data-slick-scrollbar on containers is far more maintainable than a global *::-webkit-scrollbar override, and it keeps the blast radius small when the package misbehaves.
Wrap it into a Blade component. Once you know the markup contract, wrap it so views stay declarative:
{{-- resources/views/components/scroll-panel.blade.php --}}
@props(['height' => '24rem'])
<div {{ $attributes->merge(['class' => 'relative overflow-y-auto', 'data-slick-scrollbar' => true]) }}
style="max-height: {{ $height }};">
{{ $slot }}
</div>
Usage: <x-scroll-panel height="32rem">...</x-scroll-panel>. This becomes the single seam for swapping or removing the package later.
Livewire / Turbo / Inertia. Any time the DOM is replaced outside a full page load, re-run initialization against the new subtree:
document.addEventListener('livewire:navigated', () => initScrollbars());
Livewire.hook('morph.updated', ({ el }) => initScrollbars(el));
For Inertia, hook router.on('finish', ...) or call initScrollbars() inside onMounted of the page component.
Alpine.js integration works well because Alpine already owns per-element lifecycle:
<div
x-data
x-init="window.initScrollbars($el.parentElement)"
class="overflow-y-auto max-h-96">
...
</div>
Theming via CSS variables. Custom scrollbar libraries almost always expose colors/widths as CSS custom properties. Define them once in your Tailwind theme layer and switch them under .dark:
:root {
--slick-scrollbar-thumb: theme('colors.slate.300');
--slick-scrollbar-track: transparent;
--slick-scrollbar-width: 8px;
}
.dark {
--slick-scrollbar-thumb: theme('colors.slate.600');
}
This avoids forking the package just to change a color.
Progressive enhancement. Always keep the container scrollable without the package — i.e., real overflow-y: auto and a max-height. If the JS fails to load (ad blockers, CSP, old browser), users still get native scrolling. Never make visibility or truncation depend on the package running.
It's frontend-only — don't hunt for Eloquent or config magic. There's no model, no migration, no queue integration. The composer.json presence is just a delivery mechanism. If you were expecting server-side behavior, you're looking at the wrong package.
License is NOASSERTION. Composer reports no recognized SPDX license. Before shipping commercially, open the repo's LICENSE file directly and confirm the terms. If there isn't one, treat it as all-rights-reserved and consider whether you can legally vendor it — or reimplement the 200-line behavior yourself.
13 stars, 0 dependents, single maintainer. This is a bus-factor-of-one utility. Pin the exact version in composer.json ("muazzam/slick-scrollbar": "1.x.x" style) and, given how small the surface area is, consider copying the CSS/JS into resources/ rather than depending on Composer at all. You get the code, no supply-chain risk, and full control.
Re-initialization is the #1 bug source. Livewire morphs, Turbo frames, and v-if/x-show toggles will leave you with orphaned instances or duplicated scrollbars. Always guard with a data-* flag (as above) or call the library's destroy() if it exposes one, before re-init.
CSS specificity fights with Tailwind. Custom scrollbar libraries typically inject ::-webkit-scrollbar rules with element or class selectors. Tailwind's scrollbar-* utilities (if you add the plugin) or your own overrides may lose. Keep the package's selectors scoped under your data attribute and use @layer if you're on Tailwind v3+ to control ordering.
Bundle it through Vite, don't hand-publish twice. If you vendor:publish assets and import them through Vite, you'll load two copies and wonder why theming partially applies. Pick one pipeline. Vite is preferable for cache-busting and tree-shaking.
Watch for layout shift on hydration. If the JS measures container size on init, expect a flicker on first paint. Mitigate by setting explicit max-height/height on the container in CSS rather than letting the script infer it.
Performance in long lists. One instance per container is fine; hundreds (e.g., a card grid each with its own scroll area) will tank scroll performance. Use the package only on the handful of regions that actually need custom styling, and let native scrollbars handle the rest.
CSP and inline scripts. Some packages inject a <style> tag or use element.style writes. Strict Content-Security-Policy setups (style-src without 'unsafe-inline') may block the theming. Check the browser console the first time you load a page — that's your fastest diagnostic.
Debugging checklist when the scrollbar doesn't appear:
scrollHeight > clientHeight — check in DevTools.)initScrollbars run after the element existed? (Add a console.log count.)overflow?public/vendor/...?Extension point to plan for. Because the package is tiny and lightly maintained, wrap access behind your own x-scroll-panel component and one initScrollbars() function. When you inevitably swap it out — for OverlayScrollbars, simplebar, or pure CSS scrollbar-color — you change two files, not every view.
How can I help you explore Laravel packages today?