Questions
This modal is clipped by its parent. Fix it using a React Portal.
A developer has implemented a Modal component. It’s designed to pop up and display important information. However, when it’s used inside a Card component that has overflow: hidden and a specific z-index applied, the modal gets clipped or appears behind other elements on the page. This makes the modal unusable.
The problem is that the modal is rendered within the DOM hierarchy of its parent (Card), inheriting its parent’s styling properties that prevent it from displaying correctly as an overlay.
You’ve been given the code for the App and Modal components. Your task is to fix this issue by using a React Portal to render the modal outside the parent’s DOM hierarchy. This will ensure it appears correctly as an overlay, fully visible and on top of all other content.
import React, { useState } from 'react';
import ReactDOM from 'react-dom'; // We'll need this for Portals
// The Modal component - currently renders within its parent's DOM hierarchy
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
// THE BUG: This modal renders directly inside its parent,
// making it susceptible to parent's CSS properties like overflow and z-index.
return (
<div style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
zIndex: 1000, // This z-index might not be enough if parent has higher
maxWidth: '80vw',
maxHeight: '80vh',
overflow: 'auto',
}}>
{children}
<button onClick={onClose} style={{ position: 'absolute', top: '10px', right: '10px' }}>
X
</button>
</div>
);
}
// A Card component with styling that will clip the modal
function Card({ children }) {
return (
<div style={{
border: '1px solid #ccc',
padding: '20px',
width: '300px',
height: '200px',
margin: '50px auto',
overflow: 'hidden', // This is the culprit!
position: 'relative',
zIndex: 1,
}}>
{children}
</div>
);
}
export default function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<h1>Main Application Content</h1>
<Card>
<h2>Content inside Card</h2>
<p>This card has overflow: hidden.</p>
<button onClick={() => setIsModalOpen(true)}>Open Clipped Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h3>Important Message</h3>
<p>This modal content should be fully visible!</p>
<p>But it's being clipped by the card.</p>
</Modal>
</Card>
{/* We need a target DOM node for the portal. Add this to your index.html:
<div id="modal-root"></div>
For this runner, we'll simulate it.
*/}
<div id="modal-root" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}></div>
</div>
);
} How Different Experience Levels Approach This
I would change the Card's CSS to remove `overflow: hidden` or adjust the z-index values to make the modal appear on top.
I would use `ReactDOM.createPortal` to render the modal into a separate DOM node outside the Card's hierarchy, typically directly under the body. This breaks the modal free from its parent's CSS constraints (like overflow and z-index context) while maintaining React's component hierarchy and event bubbling, making it a proper overlay solution.
The problem arises because the Modal component is rendered directly within the Card component’s DOM hierarchy. When a parent element has CSS properties like overflow: hidden or a specific z-index context, its children are constrained by those properties. This is why the modal gets clipped or appears behind other elements.
React Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This is perfect for modals, tooltips, and other overlays that need to “break out” of their parent’s styling constraints.
To use a Portal:
- You need a target DOM node outside your main React application’s root. This is typically an empty
divelement added directly to yourindex.html(e.g.,<div id="modal-root"></div>). - Inside your component (e.g.,
Modal), you useReactDOM.createPortal(child, domNode)to render the modal’s content into that external DOM node.
Here is the corrected implementation:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
// The Modal component - now uses a Portal
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
// Ensure the modal root element exists
const modalRoot = document.getElementById('modal-root');
if (!modalRoot) {
// In a real app, you'd ensure this exists or create it dynamically
console.error("Modal root element not found! Please add <div id='modal-root'></div> to your index.html");
return null;
}
// FIX: Use ReactDOM.createPortal to render the modal content
// into the 'modal-root' DOM node.
return ReactDOM.createPortal(
<div style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
zIndex: 1000,
maxWidth: '80vw',
maxHeight: '80vh',
overflow: 'auto',
}}>
{children}
<button onClick={onClose} style={{ position: 'absolute', top: '10px', right: '10px' }}>
X
</button>
</div>,
modalRoot // Render into the external DOM node
);
}
// A Card component with styling that will clip the modal
function Card({ children }) {
return (
<div style={{
border: '1px solid #ccc',
padding: '20px',
width: '300px',
height: '200px',
margin: '50px auto',
overflow: 'hidden',
position: 'relative',
zIndex: 1,
}}>
{children}
</div>
);
}
export default function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
// Ensure the modal-root div exists in the DOM for the runner
useEffect(() => {
let modalRoot = document.getElementById('modal-root');
if (!modalRoot) {
modalRoot = document.createElement('div');
modalRoot.id = 'modal-root';
document.body.appendChild(modalRoot);
}
return () => {
if (modalRoot && modalRoot.parentNode === document.body) {
document.body.removeChild(modalRoot);
}
};
}, []);
return (
<div>
<h1>Main Application Content</h1>
<Card>
<h2>Content inside Card</h2>
<p>This card has overflow: hidden.</p>
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h3>Important Message</h3>
<p>This modal content should be fully visible!</p>
</Modal>
</Card>
</div>
);
}With the Modal component now using ReactDOM.createPortal, its content is rendered directly under the body element (or whatever modalRoot is), effectively “breaking out” of the Card’s overflow: hidden and z-index constraints. This ensures the modal always appears as a top-level overlay.
- Context over facts: Explains when and why, not just what
- Real examples: Provides specific use cases from production experience
- Trade-offs: Acknowledges pros, cons, and decision factors
Practice Question
What is the primary use case for React Portals?