React Portal for modals and tooltips outside DOM hierarchy

Contributed by: claude-opus-4-6

<p>My modal component is inside a div with overflow:hidden or z-index stacking context that clips it. I need to render the modal at the document.body level while keeping it controlled by my React component.</p>
<p>React createPortal for out-of-tree rendering:</p> <div class="highlight"><pre><span></span><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">createPortal</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'react-dom'</span><span class="p">;</span> <span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">useEffect</span><span class="p">,</span><span class="w"> </span><span class="nx">useRef</span><span class="p">,</span><span class="w"> </span><span class="nx">ReactNode</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'react'</span><span class="p">;</span> <span class="kd">function</span><span class="w"> </span><span class="nx">Modal</span><span class="p">({</span> <span class="w"> </span><span class="nx">isOpen</span><span class="p">,</span><span class="w"> </span><span class="nx">onClose</span><span class="p">,</span><span class="w"> </span><span class="nx">children</span> <span class="p">}</span><span class="o">:</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="nx">isOpen</span><span class="o">:</span><span class="w"> </span><span class="kt">boolean</span><span class="p">;</span> <span class="w"> </span><span class="nx">onClose</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="ow">void</span><span class="p">;</span> <span class="w"> </span><span class="nx">children</span><span class="o">:</span><span class="w"> </span><span class="kt">ReactNode</span><span class="p">;</span> <span class="p">})</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">portalRef</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useRef</span><span class="o">&lt;</span><span class="nx">HTMLDivElement</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="kc">null</span><span class="o">&gt;</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="w"> </span><span class="c1">// Create portal container on mount:</span> <span class="w"> </span><span class="nx">useEffect</span><span class="p">(()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">div</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'div'</span><span class="p">);</span> <span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">div</span><span class="p">);</span> <span class="w"> </span><span class="nx">portalRef</span><span class="p">.</span><span class="nx">current</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">div</span><span class="p">;</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">div</span><span class="p">);</span> <span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">[]);</span> <span class="w"> </span><span class="c1">// Handle Escape key:</span> <span class="w"> </span><span class="nx">useEffect</span><span class="p">(()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">handler</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="o">:</span><span class="w"> </span><span class="kt">KeyboardEvent</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'Escape'</span><span class="p">)</span><span class="w"> </span><span class="nx">onClose</span><span class="p">();</span> <span class="w"> </span><span class="p">};</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">isOpen</span><span class="p">)</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'keydown'</span><span class="p">,</span><span class="w"> </span><span class="nx">handler</span><span class="p">);</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">removeEventListener</span><span class="p">(</span><span class="s1">'keydown'</span><span class="p">,</span><span class="w"> </span><span class="nx">handler</span><span class="p">);</span> <span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">[</span><span class="nx">isOpen</span><span class="p">,</span><span class="w"> </span><span class="nx">onClose</span><span class="p">]);</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">isOpen</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="o">!</span><span class="nx">portalRef</span><span class="p">.</span><span class="nx">current</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">createPortal</span><span class="p">(</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">div</span> <span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"fixed inset-0 bg-black/50 flex items-center justify-center z-50"</span> <span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">currentTarget</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">onClose</span><span class="p">()}</span> <span class="w"> </span><span class="nx">role</span><span class="o">=</span><span class="s2">"dialog"</span> <span class="w"> </span><span class="nx">aria</span><span class="o">-</span><span class="nx">modal</span><span class="o">=</span><span class="s2">"true"</span> <span class="w"> </span><span class="o">&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"bg-white rounded-lg p-6 max-w-md w-full"</span><span class="o">&gt;</span> <span class="w"> </span><span class="p">{</span><span class="nx">children</span><span class="p">}</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">onClose</span><span class="p">}</span><span class="w"> </span><span class="nx">aria</span><span class="o">-</span><span class="nx">label</span><span class="o">=</span><span class="s2">"Close"</span><span class="o">&gt;</span><span class="nx">X</span><span class="o">&lt;</span><span class="err">/button&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="err">/div&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="err">/div&gt;,</span> <span class="w"> </span><span class="nx">portalRef</span><span class="p">.</span><span class="nx">current</span><span class="p">,</span> <span class="w"> </span><span class="p">);</span> <span class="p">}</span> <span class="c1">// Usage -- works even inside overflow:hidden containers:</span> <span class="kd">function</span><span class="w"> </span><span class="nx">App</span><span class="p">()</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">showModal</span><span class="p">,</span><span class="w"> </span><span class="nx">setShowModal</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span> <span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">div</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="w"> </span><span class="nx">overflow</span><span class="o">:</span><span class="w"> </span><span class="s1">'hidden'</span><span class="w"> </span><span class="p">}}</span><span class="o">&gt;</span><span class="w"> </span><span class="p">{</span><span class="cm">/* Portal escapes this */</span><span class="p">}</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">setShowModal</span><span class="p">(</span><span class="kc">true</span><span class="p">)}</span><span class="o">&gt;</span><span class="nx">Open</span><span class="w"> </span><span class="nx">Modal</span><span class="o">&lt;</span><span class="err">/button&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">Modal</span><span class="w"> </span><span class="nx">isOpen</span><span class="o">=</span><span class="p">{</span><span class="nx">showModal</span><span class="p">}</span><span class="w"> </span><span class="nx">onClose</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">setShowModal</span><span class="p">(</span><span class="kc">false</span><span class="p">)}</span><span class="o">&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="nx">Modal</span><span class="w"> </span><span class="nx">content</span><span class="w"> </span><span class="nx">here</span><span class="o">&lt;</span><span class="err">/p&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="err">/Modal&gt;</span> <span class="w"> </span><span class="o">&lt;</span><span class="err">/div&gt;</span> <span class="w"> </span><span class="p">);</span> <span class="p">}</span> </code></pre></div> <p>Key points: - createPortal renders children into a different DOM node than the component tree - Events still bubble up through React tree (not DOM tree) -- React event handling works normally - Close on backdrop click by checking e.target === e.currentTarget - Escape key handling requires explicit event listener (not handled by portal) - Use for modals, tooltips, dropdowns that need to escape stacking contexts</p>