ProgramGuru

Render Outside a DOM Container with Portals

Render Outside a DOM Container with Portals

A reminder belongs to the school dashboard, but a surrounding panel clips content at its edges. Rendering the reminder into a different DOM container can avoid that placement constraint without moving its state into an unrelated application.

A portal renders React children into another DOM node. The physical browser placement changes, while their React-tree relationship to the parent remains. A portal is a placement tool, not a complete dialog implementation.

  1. Change placement

    Choose an existing DOM destination.

  2. Keep ownership

    Understand state, context and event relationships.

  3. Respect interaction

    Separate a nonmodal notice from a modal dialog.

Follow the user’s interaction

  1. State

    App decides whether the notice appears.

  2. Portal

    The notice is placed under document.body.

  3. Dismiss

    The notice’s button updates its React owner.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

React ownership and DOM placement can differ

Normally a component’s returned elements appear inside its parent’s DOM output. createPortal(children, target) gives those children a different physical destination. The target must already exist, and the browser needs to be available when the portal is created.

Context and React event propagation still follow the React tree. Moving a notice under document.body does not make it a separate state owner or remove it from its provider. React’s createPortal reference explains the distinction between physical placement and logical ownership.

Open and dismiss a nonmodal school notice

Initially only the dashboard heading and Show reminder button appear. Selecting the button renders an aside under document.body. Dismiss reminder changes the same App state back to false, removing the portal. The complete example includes the state owner, content and destination.

This is a browser-only nonmodal notice after activation. It does not prevent interaction with the dashboard and is not labeled as a modal dialog. Its local style makes the placement visible; it is not a promise that every host page’s stacking and theme rules will be identical.

React · JavaScript with JSX

import React, { useState } from 'react';
import { createPortal } from 'react-dom';
export default function App() {
  const [show, setShow] = useState(false);
  return <main><h1>School dashboard</h1>
    <button onClick={() => setShow(true)}>Show reminder</button>
    {show && createPortal(
      <aside aria-label="School reminder" style={{ position: 'fixed', right: 16, bottom: 16, background: 'white', padding: 16, border: '1px solid' }}>
        <p role="status">Return library books on Friday</p>
        <button onClick={() => setShow(false)}>Dismiss reminder</button>
      </aside>, document.body)}
  </main>;
}

Expected screen and behavior

Initially: School dashboard and Show reminder.
Show reminder displays Return library books on Friday and Dismiss reminder outside the root container.

The dismiss handler still belongs to App

React · the portal expression and owner

import React, { useState } from 'react';
import { createPortal } from 'react-dom';
export default function App() {
  const [show, setShow] = useState(false);
  return <main><h1>School dashboard</h1>
    <button onClick={() => setShow(true)}>Show reminder</button>
    {show && createPortal(
      <aside aria-label="School reminder" style={{ position: 'fixed', right: 16, bottom: 16, background: 'white', padding: 16, border: '1px solid' }}>
        <p role="status">Return library books on Friday</p>
        <button onClick={() => setShow(false)}>Dismiss reminder</button>
      </aside>, document.body)}
  </main>;
}

When show is false, React does not evaluate the right-hand portal expression. That keeps document.body out of the initial render path, which is useful when discussing environments without a browser. Once the event sets show to true in a mounted browser, createPortal receives the existing body node.

The dismiss button closes over setShow just like an ordinary child button. The notice can also read a surrounding context because it remains a React descendant. Do not create a second React root merely to display this notice unless you intentionally need a separate application boundary.

Events follow the logical tree

A React onClick on an ancestor may still observe a click inside the portal. That can surprise a developer inspecting only the DOM, where the ancestor does not physically contain the notice. If an ancestor handler unintentionally closes a panel or selects a row, examine the React ownership path.

Use an intentional event policy: move the portal’s logical owner, keep an ancestor handler narrowly focused, or stop propagation only when that behavior is actually needed. Blindly adding stopPropagation everywhere can prevent other expected interactions. Inspect both the DOM tree and component tree when debugging placement-related behavior.

Worked practice: change the notice’s content

Use the same portal mechanism for an exam reminder. Keep the dismiss action and nonmodal meaning. The complete worked answer changes the message to Bring a pencil on Monday without creating another state variable or root.

Opening the reminder reveals the new text; dismissing removes it. There is no hidden server notification or scheduled reminder delivery. The action changes only this browser’s displayed notice, so refreshing starts from the initial closed state again.

React · JavaScript with JSX

import React, { useState } from 'react';
import { createPortal } from 'react-dom';
export default function App() {
  const [show, setShow] = useState(false);
  return <main><h1>School dashboard</h1>
    <button onClick={() => setShow(true)}>Show reminder</button>
    {show && createPortal(
      <aside aria-label="School reminder" style={{ position: 'fixed', right: 16, bottom: 16, background: 'white', padding: 16, border: '1px solid' }}>
        <p role="status">Bring a pencil on Monday</p>
        <button onClick={() => setShow(false)}>Dismiss reminder</button>
      </aside>, document.body)}
  </main>;
}

Expected screen and behavior

Initially: School dashboard and Show reminder.
Opening displays Bring a pencil on Monday.

A portal alone does not make a dialog accessible

A modal dialog requires more than a fixed position and an aria-modal attribute. It needs an appropriate label, focus entry and containment, keyboard dismissal and sensible return focus, plus interaction restrictions outside the dialog. Use an established accessible dialog implementation or the native dialog API with a complete policy.

The destination can disappear or conflict with another application’s styling. Choose a stable container and clean up owned infrastructure deliberately. Server rendering cannot create an ordinary DOM portal into document.body without a browser; framework integrations need their own handling. The initially closed example should not be mistaken for a universal server portal implementation.

Check portal ownership

Does placing the notice under document.body disconnect it from React context?
Choose one answer. You can try again.

Answer and explanation

No, the notice retains its React-tree relationship.

Correct. A portal changes DOM placement rather than logical ownership.

Apply the dialog distinction

Which statement about this reminder is accurate?
Choose one answer. You can try again.

Answer and explanation

It is a nonmodal notice whose button updates App’s state.

Correct. The example deliberately retains the ordinary dashboard interaction.

Move the DOM output while preserving ownership

Use createPortal with an existing destination when physical placement needs to differ from React ownership. State, context and React event behavior remain connected to the component tree. Add a deliberate accessibility policy for dialogs rather than expecting placement to supply it.

Review DOM integration with refs and accessible controls and focus when building larger overlays.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Understand React Performance →