useScrollLock
React hook that locks and unlocks scroll on target elements with width reflow prevention and automatic cleanup.
Installation
npx shadcn@latest add https://www.shadcn.io/registry/use-scroll-lock.json
npx shadcn@latest add https://www.shadcn.io/registry/use-scroll-lock.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/use-scroll-lock.json
bunx shadcn@latest add https://www.shadcn.io/registry/use-scroll-lock.json
Features
- Automatic locking - Locks scroll when component mounts by default
- Manual control - Optional manual lock/unlock functions
- Width reflow prevention - Prevents layout shift by compensating for scrollbar width
- Custom targets - Lock scroll on specific elements or body
- Style restoration - Restores original styles when unlocking
- SSR compatible - Safely handles server-side rendering
- Cleanup on unmount - Automatically unlocks scroll when component unmounts
Usage
import { useScrollLock } from "@/hooks/use-scroll-lock"
// Auto-lock on mount (perfect for modals)
function Modal() {
useScrollLock()
return <div>Modal content</div>
}
// Manual control
function App() {
const { isLocked, lock, unlock } = useScrollLock({ autoLock: false })
return (
<div>
<button onClick={lock}>Lock Scroll</button>
<button onClick={unlock}>Unlock Scroll</button>
<p>Status: {isLocked ? "Locked" : "Unlocked"}</p>
</div>
)
}
API Reference
useScrollLock
useScrollLock(options?: UseScrollLockOptions): UseScrollLockReturn
UseScrollLockOptions
Property | Type | Default | Description |
---|---|---|---|
autoLock | boolean | true | Whether to lock scroll automatically on mount |
lockTarget | HTMLElement | string | document.body | Target element to lock (element or CSS selector) |
widthReflow | boolean | true | Whether to prevent width reflow by adding padding |
UseScrollLockReturn
Property | Type | Description |
---|---|---|
isLocked | boolean | Current lock state |
lock | () => void | Function to lock scroll |
unlock | () => void | Function to unlock scroll |
Usage Examples
Auto-Lock Modal
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
// Automatically locks body scroll when modal is open
useScrollLock()
if (!isOpen) return null
return (
<div className="modal-overlay">
<div className="modal">
<h2>Modal Title</h2>
<p>Body scroll is locked while this modal is open</p>
<button onClick={onClose}>Close</button>
</div>
</div>
)
}
Manual Control
function ScrollableContent() {
const { isLocked, lock, unlock } = useScrollLock({
autoLock: false,
lockTarget: "#scrollable-area"
})
return (
<div>
<div id="scrollable-area" style={{ height: 200, overflow: "auto" }}>
{/* Long content */}
</div>
<button onClick={isLocked ? unlock : lock}>
{isLocked ? "Unlock" : "Lock"} Scroll
</button>
</div>
)
}
Custom Target Element
function CustomScrollLock() {
const scrollRef = useRef<HTMLDivElement>(null)
const { isLocked, lock, unlock } = useScrollLock({
autoLock: false,
lockTarget: scrollRef.current,
widthReflow: false // Disable width reflow for custom elements
})
return (
<div>
<div ref={scrollRef} className="custom-scroll-area">
Content
</div>
<button onClick={lock}>Lock Custom Area</button>
</div>
)
}
Sidebar/Drawer
function Sidebar({ isOpen }: { isOpen: boolean }) {
useScrollLock({ autoLock: isOpen })
return (
<div className={`sidebar ${isOpen ? "open" : "closed"}`}>
<p>Sidebar content</p>
<p>Body scroll locked when sidebar is open</p>
</div>
)
}
Full-Screen Loading
function FullScreenLoader({ isLoading }: { isLoading: boolean }) {
useScrollLock({ autoLock: isLoading })
if (!isLoading) return null
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="spinner">Loading...</div>
</div>
)
}
Photo Gallery
function PhotoGallery() {
const [selectedPhoto, setSelectedPhoto] = useState<string | null>(null)
// Lock scroll when photo is selected
useScrollLock({ autoLock: !!selectedPhoto })
return (
<div>
<div className="photo-grid">
{photos.map(photo => (
<img
key={photo.id}
onClick={() => setSelectedPhoto(photo.src)}
src={photo.thumbnail}
alt={photo.alt}
/>
))}
</div>
{selectedPhoto && (
<div className="photo-lightbox" onClick={() => setSelectedPhoto(null)}>
<img src={selectedPhoto} alt="Full size" />
</div>
)}
</div>
)
}
Conditional Locking
function ConditionalLock() {
const [shouldLock, setShouldLock] = useState(false)
const isMobile = useMediaQuery("(max-width: 768px)")
// Only lock scroll on mobile devices
useScrollLock({
autoLock: shouldLock && isMobile,
widthReflow: !isMobile // Only prevent reflow on desktop
})
return (
<div>
<button onClick={() => setShouldLock(!shouldLock)}>
Toggle Lock {isMobile ? "(Mobile)" : "(Desktop)"}
</button>
</div>
)
}
Common Use Cases
- Modal dialogs - Prevent background scrolling when modal is open
- Sidebar/drawer navigation - Lock body scroll when side panel is active
- Full-screen overlays - Loading screens, image lightboxes
- Form wizards - Multi-step forms that shouldn't allow background interaction
- Video players - Full-screen video modes
- Game interfaces - Prevent accidental scrolling during gameplay
- Mobile menus - Prevent body scroll when mobile menu is open
- Infinite scroll pausing - Temporarily disable scroll during data loading
Implementation Details
- Uses
overflow: hidden
to lock scroll - Calculates scrollbar width and adds equivalent padding to prevent layout shift
- Stores original styles in refs for proper restoration
- Uses
useIsomorphicLayoutEffect
to avoid SSR issues - Automatically unlocks scroll when component unmounts
- Supports both CSS selectors and direct element references
- Handles edge cases like missing target elements gracefully
Width Reflow Prevention
When widthReflow
is enabled (default):
- Calculates the scrollbar width by comparing
offsetWidth
andscrollWidth
- Adds padding-right to compensate for the hidden scrollbar
- Uses
window.innerWidth
for body element to account for global scrollbar - Preserves existing padding-right values by adding to them
Best Practices
- Use auto-lock for modals: Set
autoLock: true
for modal components - Manual control for toggles: Use manual control for toggleable content
- Disable reflow for custom elements: Set
widthReflow: false
for non-body targets - Handle mobile separately: Consider different behavior on mobile devices
- Test with existing padding: Ensure the hook works with elements that already have padding
- Clean up properly: The hook automatically cleans up, but be mindful of component lifecycles
useScript
Custom hook that dynamically loads scripts and tracks their loading status with caching and cleanup support. Perfect for React applications requiring third-party libraries with Next.js integration and TypeScript support.
useSessionStorage
Custom hook that uses the sessionStorage API to persist state across page reloads with serialization and cross-tab synchronization. Perfect for React applications requiring temporary state persistence with Next.js integration and TypeScript support.