Sidebar Drawer Portal
Sidebar Drawer Portal
A sidebar drawer slides in from the left or right side of the viewport. Like modals, drawers
need to be outside the component tree’s DOM container to avoid overflow or z-index issues.
Drawer.jsx
import { createPortal } from 'react-dom';
import { useEffect } from 'react';
function Drawer({ isOpen, onClose, side = 'right', children }) {
// Lock background scroll
useEffect(() => {
document.body.style.overflow = isOpen ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [isOpen]);
// Escape to close
useEffect(() => {
const handler = (e) => e.key === 'Escape' && onClose();
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [onClose]);
if (!isOpen) return null;
return createPortal(
<>
{/* Backdrop */}
<div
onClick={onClose}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.4)',
zIndex: 8888
}}
/>
{/* Drawer panel */}
<div
role="dialog"
aria-modal="true"
style={{
position: 'fixed',
top: 0,
bottom: 0,
[side]: 0,
width: '320px',
background: '#fff',
boxShadow: side === 'right' ? '-4px 0 16px rgba(0,0,0,0.2)' : '4px 0 16px rgba(0,0,0,0.2)',
zIndex: 9999,
overflowY: 'auto',
padding: '1.5rem'
}}
>
<button onClick={onClose} aria-label="Close drawer">✕</button>
{children}
</div>
</>,
document.body
);
}
export default Drawer;
Usage
import { useState } from 'react';
import Drawer from './Drawer';
function App() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(true)}>Open Drawer</button>
<Drawer isOpen={open} onClose={() => setOpen(false)} side="right">
<h2>Sidebar Navigation</h2>
<ul>
<li>Home</li>
<li>Profile</li>
<li>Settings</li>
</ul>
</Drawer>
</div>
);
}
