Let’s talk about a small UX detail that can make a big difference.
Imagine your application has a modal. The user opens it… but the background page is still scrollable. Suddenly, the focus is gone, the UI feels messy, and the experience isn’t as pleasant as it should be.
This is a surprisingly common issue and a perfect example of where useEffect can help us clean things up and improve the user experience.
In this post, I’ll show a simple and effective way to prevent background scrolling when a modal is open, using React’s useEffect.
Here’s what it looks like when the background screen is still scrollable.
Now, let’s take a closer look at how useEffect solves this problem
useEffect(() => {
// This effect runs when the modal is mounted (opened).
// It disables background scrolling by hiding the page overflow.
document.body.style.overflow = 'hidden';
return () => {
// This cleanup function runs when the modal is unmounted (closed).
// It restores the original scrolling behavior.
document.body.style.overflow = 'unset';
};
}, []); // The empty dependency array ensures this runs only once
if you prefer here is the live code: https://stackblitz.com/~/github.com/LeonardoPalmeiro/vitejs-vite-5ccuxsat
more information about useEffect hook:
Modal.jsx
import { useEffect } from 'react';
function Modal({ onClose }) {
useEffect(() => {
// This effect runs when the modal is mounted (opened).
// It disables background scrolling by hiding the page overflow.
document.body.style.overflow = 'hidden';
return () => {
// This cleanup function runs when the modal is unmounted (closed).
// It restores the original scrolling behavior.
document.body.style.overflow = 'unset';
};
}, []); // The empty dependency array ensures this runs only once
return (
<div className="modal-backdrop">
<div className="modal-container">
<button className="modal-close" onClick={onClose}>
×
</button>
<h2 className="modal-title">Modal Title</h2>
<p className="modal-body">
This is a centered modal with clean styling.
</p>
<div className="modal-actions">
<button onClick={onClose}>Close</button>
</div>
</div>
</div>
);
}
export default Modal;
App.jsx
import './App.css';
import Modal from './modal';
import { useState } from 'react';
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
{/* Add a tall container to enable scrolling */}
<div style={{ height: '200vh', background: 'linear-gradient(white, gray)' }}>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<Modal onClose={() => setIsOpen(false)} />
)}
</div>
</>
);
}
export default App;
App.css
/* Backdrop */
.modal-backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
/* Modal container */
.modal-container {
position: relative;
width: 100%;
max-width: 480px;
background: #ffffff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
animation: modalFadeIn 0.25s ease-out;
}
/* Animation */
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Close button */
.modal-close {
position: absolute;
top: 12px;
right: 12px;
border: none;
background: transparent;
font-size: 24px;
cursor: pointer;
color: #666;
}
.modal-close:hover {
color: #000;
}
/* Content */
.modal-title {
margin: 0 0 12px;
font-size: 20px;
font-weight: 600;
}
.modal-body {
font-size: 15px;
color: #555;
margin-bottom: 20px;
}
/* Actions */
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* Buttons */
.btn {
padding: 8px 16px;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 500;
}
.btn.primary {
background: #2563eb;
color: white;
}
.btn.primary:hover {
background: #1e4ed8;
}
.btn.secondary {
background: #e5e7eb;
color: #111;
}
.btn.secondary:hover {
background: #d1d5db;
}
/* Mobile */
@media (max-width: 480px) {
.modal-container {
margin: 16px;
padding: 20px;
}
}

Top comments (0)