Website Popups

How to Create a React Popup & Customize it in 4 Steps

A React popup is a component that shows content on top of a page without leaving it. In React 19, the simplest reliable build is a native <dialog> opened with showModal() from a useState flag: the browser adds the backdrop, Escape-to-close and an inert page behind, and your component returns focus on close.

AI summarygeneral summary

A React popup is a component that overlays content without leaving the page. In React 19 the simplest reliable build is a native dialog element opened with showModal() from a useState flag, styled with CSS, with focus returned on close; libraries like MUI, Headless UI and Radix suit design systems, while reactjs-popup hasn't had a release since 2023.

Article facts

Written by
Berna Partal
Reviewed by
Mert Özeren
Published
December 20, 2023
Updated
October 2, 2026
Sections
9
Sources
9 cited

29 min read

Our editorial standardsHow we research, source and update every post.

Creating popups in React is a common need for web applications, and React's component-based architecture makes it straightforward to build reusable ones. A React popup keeps users in context while collecting input or surfacing information. You might need a modal, an alert box or a custom form popup. This guide walks through the full process: how to build one in four steps, how to make it accessible, and which libraries to consider depending on your project's needs.

Every code block below is TypeScript, and every one was type-checked, built and run with React 19.3, TypeScript 6.0 and Vite 8.3 (checked October 2026).

a cover image that says "how to create a react popup"

What is React Popup?

A React popup is an interactive overlay element built with React that renders content above the existing page without full navigation. It uses React's useState hook to control visibility through conditional rendering, and typically includes an overlay backdrop, a trigger event (click, hover, or timer), and a close mechanism.

React's component model means popups are reusable: write one once and use it across multiple views with different content via props.

Five core elements determine how any React popup functions:

  • Trigger Events: Popups activate on specific user actions: button clicks, mouse hovers, scroll depth, or time on page. An event handler or an effect sets the popup's state when the trigger fires.
  • State Management: React's useState hook manages visibility. The open boolean drives conditional rendering to show or hide the popup based on user interactions.
  • Overlay Backdrop: A semi-transparent layer behind the popup prevents interaction with the underlying page, focusing user attention on the popup content.
  • Customizable Elements: CSS styling, animation transitions, and props configuration make popup components reusable across different contexts and visual themes.
  • Accessibility Features: ARIA roles, focus trapping, and keyboard support make popups usable for all users, including those on screen readers. Skipping these is one of the most common implementation mistakes.
How a React popup works in four steps: click the trigger, state changes, showModal() opens the dialog, close returns focus
The four steps behind the popup component in this guide.

The flow behind every example in this guide has four steps. First, you click the trigger: onClick calls setOpen(true). Then the state changes: open is true and React re-renders. Next, showModal() opens the dialog, with the backdrop on and the page behind inert. Finally, close returns focus: Esc, the backdrop or the Close button shuts the popup and focus goes back to the trigger.

One key advantage of React popups over full page loads is context maintenance.

Instead of redirecting users to a new page or reloading to show a message, a React modal popup overlays content in place: a sign-up form appears on click, collects input, and closes without reloading the page. Users stay in whatever flow they were already in. For e-commerce and SaaS applications, where mid-flow interruptions cause drop-offs, this is a meaningful behavioral difference.

React portals are worth understanding early: they render popup content outside the main DOM hierarchy (typically appended to document.body), which solves z-index conflicts and CSS inheritance issues that arise in nested component trees. For any popup that needs to sit above fixed headers, sidebars, or complex layouts, portals are the right approach. The portal child still belongs to the React tree, so context and events keep working. For a modal, the native <dialog> goes one step further: showModal() puts it in the browser's top layer, above every z-index on the page, so it needs no portal at all.

React Native is the exception. It has no DOM, so <dialog>, createPortal from react-dom and the CSS ::backdrop don't exist there. Use React Native's built-in Modal component instead: it takes visible, animationType and transparent, and onRequestClose is required on Android, where the hardware back button calls it. The state pattern carries over: a useState boolean drives visible.

What Are the Challenges of Implementing React Popups Effectively?

Most popup bugs fall into predictable categories. Knowing them before you write code saves a lot of debugging, and these are the ones that trip up implementations most often:

  • State management conflicts: useState outgrows local scope fast. Popups that need to open once per session, or depend on auth status, require lifted state via Context or a global manager like Redux or Zustand.
  • Accessibility gaps: Missing focus trapping lets keyboard users tab into the obscured page behind the popup. Missing ARIA attributes (role="dialog", aria-modal="true", aria-labelledby) break screen reader interpretation entirely; these aren't optional.
  • CSS and z-index conflicts: Overlays nested inside complex component trees inherit unwanted stacking contexts. React portals fix this by rendering at document.body level, outside the cascade, and a <dialog> opened with showModal() avoids it entirely in the top layer.
  • Performance re-renders: Heavy popup content that re-renders on unrelated parent state changes causes jank. React.memo and useCallback on the close handler are the two most effective fixes when you're not using React Compiler.
  • Scaling complexity: What starts as one modal grows into ten popup types with different triggers and state dependencies. Planning a popup manager pattern early is easier than retrofitting one later.
  • Exit intent false positives: Raw mouseout events without debouncing fire on any fast cursor movement, not just genuine exit gestures. A 100ms debounce filters out the noise.

Common challenges and their fixes at a glance:

Challenge Root Cause Fix
Infinite re-renders useEffect dependencies missing or wrong Audit dependency arrays; use ESLint's exhaustive-deps rule
Focus escaping popup No focus trap implemented Use showModal(), or add focus trap loop on Tab/Shift+Tab keydown events
z-index conflicts Popup inside nested stacking context Use showModal() (top layer) or React portals to render at document.body level
Screen reader confusion Missing ARIA attributes Add role="dialog", aria-modal="true", aria-labelledby (a <dialog> needs only aria-labelledby)
Mobile scroll lock bugs overflow:hidden not toggled correctly Toggle body overflow in useEffect on open state change
Popup SEO penalty Intrusive interstitial on mobile Delay trigger, ensure easy dismissal, don't block content on load
Careful
Common Mistake: Copying a React 16 or 17 popup tutorial. React 19 removed findDOMNode and defaultProps on function components, so older modal and transition code that relies on them breaks. If a tutorial starts with a class component or ReactDOM.render, it was written for an older React.

How to Create a Popup with React in 4 Steps?

A laptop screen showing React code for a popup component

The following steps build a reusable React popup component with no popup library: React hooks for state, the native <dialog> element for the overlay, and plain CSS. <dialog> has been supported across browsers since March 2022 (MDN marks it Baseline, widely available), so it works for nearly every visitor today.

Info
Pro Tip: Earlier versions of this guide used the reactjs-popup package. Its last release, 2.0.6, came out on 6 September 2023, and its GitHub page has asked for maintainers since January 2023. It still runs on React 19.3 in our test, but new projects are better served by the native dialog below or a maintained library (see the alternatives section).

Step 1: Set up a React 19 project

Run the following in your terminal to create a React and TypeScript project with Vite via npm:

npm create vite@latest react-popup -- --template react-ts
cd react-popup
npm install
npm run dev

Or with Yarn:

yarn create vite react-popup --template react-ts

The popup itself needs no extra package: useState, useEffect, useId and useRef come with React, and <dialog> comes with the browser. React ships its TypeScript types through @types/react, which the Vite template already installs. If you're working in a non-React stack, the same popup patterns apply to other frameworks; see our guides on Angular popups, Bootstrap popups and modal popups in Bootstrap, Tailwind, CSS and jQuery for equivalent implementations.

Step 2: Create a Basic Popup in React

The core pattern is a useState boolean in the parent, passed to the popup as its open prop, with a close handler passed as a callback. Create src/Popup.tsx:

import { useEffect, useId, useRef, type ReactNode } from 'react';
import './popup.css';

type PopupProps = {
  open: boolean;
  onClose: () => void;
  title: string;
  children: ReactNode;
};

export default function Popup({ open, onClose, title, children }: PopupProps) {
  const dialogRef = useRef<HTMLDialogElement>(null);
  const titleId = useId();

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!open || !dialog) return;

    const trigger = document.activeElement; // the button that opened the popup
    dialog.showModal(); // top layer, backdrop, page behind becomes inert

    return () => {
      dialog.close();
      if (trigger instanceof HTMLElement) trigger.focus(); // focus goes back
    };
  }, [open]);

  return (
    <dialog
      ref={dialogRef}
      className="popup"
      aria-labelledby={titleId}
      onClose={onClose} // fires on Escape too
      onClick={(event) => {
        if (event.target === event.currentTarget) onClose(); // click on the backdrop
      }}
    >
      <div className="popup__body">
        <h2 id={titleId}>{title}</h2>
        {children}
        <button type="button" onClick={onClose}>
          Close
        </button>
      </div>
    </dialog>
  );
}

Then use it in src/App.tsx:

import { useState } from 'react';
import Popup from './Popup';

export default function App() {
  const [open, setOpen] = useState(false);

  return (
    <main>
      <button type="button" onClick={() => setOpen(true)}>
        Open popup
      </button>
      <Popup open={open} onClose={() => setOpen(false)} title="Hello from a React popup">
        <p>Popup content here!</p>
      </Popup>
    </main>
  );
}

How it works: when open turns true, the effect calls showModal(), which puts the dialog in the top layer, draws the backdrop and makes the rest of the page inert. When open turns false, or the component unmounts, the cleanup closes the dialog and moves focus back to the button that opened it. Pressing Escape closes the dialog natively and fires its close event, which calls your onClose. A click on the backdrop lands on the <dialog> element itself (the inner div holds the padding), so that check closes it too. useId gives each popup a unique id for aria-labelledby, so screen readers announce the title.

You'll know it's working when: the page dims, the popup appears with "Hello from a React popup" and "Popup content here!", focus sits on the Close button, and Escape, a backdrop click or Close each shut it and put focus back on "Open popup".

Step 3: Customize Your React Popup

Customization happens through CSS. Create src/popup.css (the component already imports it) and apply styles that match your site's design system:

.popup {
  width: min(90vw, 300px);
  padding: 0; /* the inner div carries the padding, so a click on it never counts as a backdrop click */
  border: none;
  border-radius: 8px;
  background: #f1f1f1;
  color: #191a1a;
}

.popup__body {
  padding: 20px;
  text-align: center;
}

.popup::backdrop {
  background: rgb(0 0 0 / 0.5);
}

.popup[open] {
  animation: popup-in 200ms ease-out;
}

@keyframes popup-in {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .popup[open] {
    animation: none;
  }
}

This gives the popup a width of 300px (narrower on small phones), 20px of padding around the text, a light grey background (#f1f1f1) and centred text. The ::backdrop pseudo-element styles the dim layer behind a modal dialog, so you don't need a separate overlay div.

Keep animations under 300ms; longer transitions on interactive elements feel sluggish, particularly on mobile. The prefers-reduced-motion block turns the animation off for visitors who ask their system for less motion. If you reach for React Transition Group (last release 4.4.5, 2023), pass it a nodeRef: React 19 removed findDOMNode, which the library falls back on without one.

Step 4: Creating Different Types of Popups in React

Once the basic setup is working, the same component extends to specialized popup types. Here's a prompt popup that captures user input in real time:

import { useState, type FormEvent } from 'react';
import Popup from './Popup';

export default function PromptPopup() {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setOpen(false);
  }

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Enter name
      </button>
      <Popup open={open} onClose={() => setOpen(false)} title="Enter your name">
        <form onSubmit={handleSubmit}>
          <label>
            Name
            <input
              type="text"
              value={name}
              onChange={(event) => setName(event.target.value)}
              placeholder="Your name"
            />
          </label>
          <p>Hello, {name || 'User'}!</p>
          <button type="submit">Save</button>
        </form>
      </Popup>
    </>
  );
}

The greeting updates as the user types. React escapes {name} when it renders, so even typed HTML shows up as plain text; never pass user input to dangerouslySetInnerHTML.

And here's an exit intent popup that fires when the cursor leaves through the top of the window, towards the tabs and the browser's address bar:

import { useEffect, useState } from 'react';
import Popup from './Popup';

const SEEN_KEY = 'exit-popup-seen';

export default function ExitIntentPopup() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    let timer: number | undefined;

    function handleMouseOut(event: MouseEvent) {
      if (sessionStorage.getItem(SEEN_KEY)) return; // once per session
      // The cursor left the page through the top edge (towards the tabs and address bar).
      if (event.relatedTarget === null && event.clientY <= 0) {
        timer = window.setTimeout(() => {
          sessionStorage.setItem(SEEN_KEY, '1');
          setOpen(true);
        }, 100); // 100 ms debounce: a quick pass over the edge doesn't count
      }
    }

    function handleMouseOver() {
      window.clearTimeout(timer); // the cursor came back in time
    }

    document.addEventListener('mouseout', handleMouseOut);
    document.addEventListener('mouseover', handleMouseOver);
    return () => {
      window.clearTimeout(timer);
      document.removeEventListener('mouseout', handleMouseOut);
      document.removeEventListener('mouseover', handleMouseOver);
    };
  }, []);

  return (
    <Popup open={open} onClose={() => setOpen(false)} title="Before you go">
      <p>Get our weekly tips in your inbox.</p>
    </Popup>
  );
}

Exit intent only works with a mouse, so it never fires on phones and tablets. The 100ms delay ignores a cursor that crosses the edge and comes straight back, and sessionStorage keeps it to one showing per session.

Should you build one at all? In Popupsmart's 2026 popup benchmark, exit intent shows no statistically reliable advantage: popups on the most sensitive exit-intent setting converted 1.10% vs 0.78% without exit intent (median campaign), a gap that could be chance. Our deeper look at whether exit intent popups work breaks the data down by page type.

Popups on the most sensitive exit-intent setting converted 1.10% vs 0.78% without exit intent, median campaign; the gap is not statistically reliable and could be chance
Median campaign, Popupsmart popup benchmark 2026, 28,674 campaigns.

For a notification, a modal is the wrong tool: it steals focus for a message the user only needs to read. Render a toast through createPortal instead, with role="status" so screen readers announce it without moving focus:

import { useEffect, useEffectEvent } from 'react';
import { createPortal } from 'react-dom';

type ToastProps = {
  message: string | null;
  onDismiss: () => void;
};

export default function Toast({ message, onDismiss }: ToastProps) {
  const dismiss = useEffectEvent(onDismiss);

  useEffect(() => {
    if (!message) return;
    const timer = window.setTimeout(() => dismiss(), 4000);
    return () => window.clearTimeout(timer);
  }, [message]);

  // Keep the live region in the page even when it's empty,
  // so screen readers announce the message when it appears.
  return createPortal(
    <div role="status" className="toast">
      {message}
    </div>,
    document.body,
  );
}

Show it with const [toast, setToast] = useState<string | null>(null);, then <Toast message={toast} onDismiss={() => setToast(null)} /> and setToast('Saved') after a successful save. useEffectEvent, stable since React 19.2, lets the timer call the latest onDismiss without restarting every time the parent re-renders.

Common popup types and their primary use cases:

  • Modal Component: Full-focus overlays for confirmations, sign-up forms, or critical decisions. Always implement focus trapping; <dialog> with showModal(), as in Step 2, does it for you.
  • Notification Popup: Auto-dismissing alerts with a setTimeout close. Ideal for success/error feedback after form submissions or async operations.
  • Tooltip Popup: Hover-triggered overlays for contextual help, positioned next to their trigger, with minimal styling and no backdrop. Tooltips should never block the underlying content.
  • Exit Intent Popup: Mouse-leave detection for retention offers. Limit to one trigger per session to avoid frustrating users.

For notification popups, use role="status" (polite) for confirmations and role="alert" only for errors that need attention at once; role="alert" is announced assertively, so pairing it with aria-live="polite" sends mixed signals.

See the React createPortal documentation for DOM placement details.

How to Make React Popups Accessible?

Accessible React popups require three things working together: correct ARIA attributes, keyboard navigation support with focus trapping, and proper focus management on open and close. Each is required for WCAG 2.2 AA compliance, and for a functional experience for the more than 1 in 4 U.S. adults who have some type of disability (CDC, Disability and Health Data System, 2024 update).

A <dialog> opened with showModal() covers most of this for you. The browser gives it the dialog role, makes everything outside it inert so Tab can't reach the page behind, and closes it on Escape. You still add aria-labelledby, so the title is announced when the dialog opens, and you return focus on close, as the Step 2 component does.

If your popup has to be a plain div (a design system that renders its own overlay, for example), start with three ARIA attributes on the popup container: role="dialog" identifies it to screen readers, aria-modal="true" prevents the virtual cursor from leaving the popup, and aria-labelledby connects the dialog to its heading so the title is announced immediately on focus. Then trap focus. Focus trapping keeps keyboard navigation contained within the popup. Here's a complete implementation using useRef and useEffect, rendered through a portal:

import { useEffect, useEffectEvent, useRef, type ReactNode } from 'react';
import { createPortal } from 'react-dom';

const FOCUSABLE =
  'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

type AccessiblePopupProps = {
  open: boolean;
  onClose: () => void;
  labelledBy: string; // id of the popup's heading
  children: ReactNode;
};

export default function AccessiblePopup({ open, onClose, labelledBy, children }: AccessiblePopupProps) {
  const popupRef = useRef<HTMLDivElement>(null);
  const close = useEffectEvent(onClose);

  useEffect(() => {
    const popup = popupRef.current;
    if (!open || !popup) return;

    const trigger = document.activeElement;
    const focusable = () => Array.from(popup.querySelectorAll<HTMLElement>(FOCUSABLE));
    focusable()[0]?.focus();

    function handleKeyDown(event: KeyboardEvent) {
      if (event.key === 'Escape') {
        close();
        return;
      }
      if (event.key !== 'Tab') return;

      const items = focusable();
      if (items.length === 0) {
        event.preventDefault();
        return;
      }
      const first = items[0];
      const last = items[items.length - 1];
      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }

    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      if (trigger instanceof HTMLElement) trigger.focus();
    };
  }, [open]);

  if (!open) return null;

  return createPortal(
    <div className="popup-overlay open">
      <div ref={popupRef} role="dialog" aria-modal="true" aria-labelledby={labelledBy}>
        {children}
      </div>
    </div>,
    document.body,
  );
}

When the popup closes, focus should return to the element that triggered it. Both components in this guide do it in two lines: they read document.activeElement before opening and call focus() on it in the effect's cleanup. Without it, screen reader and keyboard users are left at the top of the page after dismissing the popup.

Tip
Key Takeaway: In React 19, start every modal popup from <dialog> and showModal(). The browser handles the backdrop, the inert page and Escape; your code adds the accessible name and returns focus to the trigger.

Key accessibility checklist items before shipping any React popup:

  • All interactive elements are reachable by keyboard Tab navigation
  • Escape key closes the popup (standard expected behavior)
  • Color contrast meets 4.5:1 ratio for body text, 3:1 for large text
  • Animations respect prefers-reduced-motion media query
  • Screen reader tested with VoiceOver (macOS) or NVDA (Windows)
  • Focus returns to the trigger element on close

Audit your implementation with Chrome Lighthouse. The Accessibility score catches missing ARIA attributes, contrast failures, and some focus problems automatically; test the keyboard flow by hand as well.

What are the Best Alternatives to reactjs-popup?

Different projects have different requirements. The native dialog in this guide works well for most popups, and reactjs-popup is still around in older codebases, but larger apps or teams with strict accessibility or design-system requirements often need a library. Here's how the main options compare, with the latest versions on npm when we checked (October 2026):

Library Latest version (release) React 19 in peer range Accessibility Built-in Best For
reactjs-popup 2.0.6 (Sep 2023) Yes (react >=16) Partial: focus and Escape handled, no aria-modal or accessible name Existing projects that already use it
Material UI (MUI) 9.4.0 (Aug 2026) Yes Strong (Dialog component) Design-system apps, enterprise projects
Headless UI 2.2.10 (Apr 2026) Yes Excellent (built-in focus trap) Teams wanting full style control + accessibility
Radix UI Dialog 1.1.23 (Jul 2026) Yes Excellent (WAI-ARIA dialog pattern) Accessible component libraries, design systems
Semantic UI React 2.1.5 (Dec 2023) No (up to React 18) Partial Projects already on Semantic UI and React 18
Native <dialog> + hooks No package Not applicable Built into the browser, plus your focus return Most popups, zero dependencies

reactjs-popup

reactjs-popup installs on React 19 without peer-dependency warnings, and in our React 19.3 test the modal version moved focus to the first button inside, kept Tab inside the popup, closed on Escape and returned focus to the trigger. It sets role="dialog" but no aria-modal and no accessible name, so add an aria-label or aria-labelledby inside the content yourself. Its last release was 2.0.6 in September 2023. If your project already uses it, the controlled version looks like this:

import { useState } from 'react';
import Popup from 'reactjs-popup';
import 'reactjs-popup/dist/index.css';

export default function LibraryPopup() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Open popup
      </button>
      <Popup
        open={open}
        onClose={() => setOpen(false)}
        modal
        closeOnDocumentClick
        contentStyle={{ width: '300px', padding: '20px', borderRadius: '8px' }}
      >
        <div>
          <p>Popup content here!</p>
          <button type="button" onClick={() => setOpen(false)}>
            Close
          </button>
        </div>
      </Popup>
    </>
  );
}

The contentStyle prop takes inline styles, and the modal prop adds the overlay. The position prop ("top center", "bottom left", "right center") only applies when you pass a trigger element and leave out modal, for tooltip-style popups.

Material UI (MUI)

Material UI is ideal if your project already uses the MUI ecosystem. Its dialog components handle accessibility concerns like focus management, keyboard navigation, and backdrop behavior automatically, making it a strong choice for enterprise apps and consistent design systems.

Headless UI

Headless UI provides fully accessible dialog primitives without enforcing styles. It's especially popular with Tailwind users who want complete visual control while still benefiting from built-in accessibility features such as focus trapping and ARIA attributes.

Custom hooks + portals

Building a custom popup solution with hooks and portals offers maximum flexibility and zero dependencies. This approach is best when performance, bundle size, or highly customized UX interactions are priorities, but it requires manual accessibility implementation. The <dialog> component in Step 2 is this approach with the browser doing most of the accessibility work.

Mobiscroll

Mobiscroll focuses on mobile-optimized popup experiences, including date pickers, selectors, and form overlays. It's a commercial library and a strong option when touch-friendly interactions and native-like animations on iOS and Android are essential. See Mobiscroll's React documentation for setup details.

Semantic UI React

Semantic UI React includes pre-styled modal and popup components that integrate well with its design system. Its last release, 2.1.5, came out in December 2023, and its peer range stops at React 18: on a React 19 project, npm install semantic-ui-react fails with an ERESOLVE error. It's a reasonable option only if the project already uses Semantic UI on React 18, and customization flexibility and maintenance activity are more limited compared to newer libraries.

Radix UI Dialog

Radix UI Dialog offers accessible, unstyled primitives designed for building component libraries and design systems. It follows the WAI-ARIA dialog pattern and gives you behavior control while leaving styling entirely to the developer.

How to choose?

  • Need lightweight simplicity: the native <dialog> component from Step 2
  • Need full accessibility with style control: Headless UI or Radix UI
  • Already using a component framework: Material UI
  • Need zero dependencies and custom behavior: Custom hooks + portals
  • Building mobile-first forms and selectors: Mobiscroll
  • Maintaining an older codebase: keep reactjs-popup or Semantic UI React until you migrate

How to Optimize React Popup Performance?

Performance problems in React popups typically come from three sources: unnecessary re-renders, blocking the main thread during animation, and heavy content loading synchronously. Each has a targeted fix.

Memoization with React.memo prevents the popup component from re-rendering when unrelated parent state changes. Wrap the popup component, and use useCallback for the close handler to keep it referentially stable. If your project runs React Compiler (1.0 since October 2025), it adds this memoization for you and you can leave both out. The example below also loads the popup's content lazily:

import { lazy, memo, Suspense, useCallback, useState } from 'react';

const PopupContent = lazy(() => import('./PopupContent'));

type LazyPopupProps = { open: boolean; onClose: () => void };

const LazyPopup = memo(function LazyPopup({ open, onClose }: LazyPopupProps) {
  if (!open) return null;
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <PopupContent onClose={onClose} />
    </Suspense>
  );
});

export default function Parent() {
  const [open, setOpen] = useState(false);
  const handleClose = useCallback(() => setOpen(false), []);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Show offer
      </button>
      <LazyPopup open={open} onClose={handleClose} />
    </>
  );
}

With src/PopupContent.tsx holding the popup itself:

import Popup from './Popup';

export default function PopupContent({ onClose }: { onClose: () => void }) {
  return (
    <Popup open onClose={onClose} title="Today's offer">
      <p>Popup content here!</p>
    </Popup>
  );
}

Vite splits PopupContent into its own small file, which the browser only downloads the first time someone clicks "Show offer".

CSS transitions over JavaScript animations offload animation work to the GPU, keeping the main thread free for user interactions. Use opacity and transform: these properties are composited by the browser without triggering layout recalculation. For a div-based popup such as the accessible one above:

.popup-overlay {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 200ms ease, transform 200ms ease;
}

.popup-overlay.open {
  opacity: 1;
  transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .popup-overlay {
    transition: none;
  }
}

Performance checklist before shipping:

  • Profile with React DevTools Profiler; look for unexpected re-renders during popup open/close
  • Use Chrome DevTools Performance panel to confirm the animation causes no layout or paint work
  • Verify the Lighthouse Performance score doesn't regress when popup is included on the page
  • Test on a mid-range Android device; it reveals performance issues that desktop testing misses
  • Throttle event listeners (scroll, mousemove) used for exit intent or scroll-triggered popups

Code splitting with dynamic imports keeps bundles lean when an app has multiple popup types. Use React.lazy() so each popup variant only loads when triggered, not on initial page load.

For complex popup state, particularly popups with multiple internal steps or form states, useReducer is a better fit than multiple useState calls.

A single reducer keeps all of a popup's state transitions in one place, so a step change can't leave related values out of sync. This is especially relevant for multi-step modals (sign-up flows, onboarding wizards), where each step changes several state values at once.

On the bundler side, tree-shaking unused popup library code reduces shipped JavaScript. Ensure your bundler configuration marks packages as side-effect-free where possible. For webpack specifically, see the webpack tree-shaking guide; Vite's production build tree-shakes ES modules by default.

How to Use Popup Builder Popupsmart for React Websites

If your team needs to create and iterate on marketing popups (a newsletter signup, an offer, a feedback form) without engineering involvement, a no-code popup builder is the faster path. Popupsmart handles targeting, design, and integrations through a visual editor, with no npm installation or state management required. It doesn't replace the components above for popups that belong to your app's own logic, such as a delete confirmation.

Step 1: Create a New Campaign

Sign up for Popupsmart; the free plan costs $0 and covers 5K pageviews a month. From the dashboard, click + New Campaign, give the campaign a name, choose your domain and click Save.

You'll be prompted to select a business objective: growing an email list, promoting an offer, collecting feedback, announcing a product, or driving phone calls. Picking the right objective shapes which templates are shown next, so the popup you build is already aligned with a conversion goal before you touch any design settings.

Secondly, you need to select a template that fits your business objective. We have many ready-to-use popup templates that are suitable for different business objectives. By exploring them, you can choose a template for the various aims of your business.

The template step in Popupsmart, where you pick a popup template by business objective

With popups created with Popupsmart, you can:

  • Grow your email list and target your visitors.
  • Show an announcement of your products and services to your customers.
  • Comply with cookie laws and take your visitors' consent for storing cookies.
  • Increase phone calls and build stronger relationships with your visitors in an old-fashioned way.
  • Promote your products and services by presenting special offers to your audience.
  • Collect form submissions to gather the details visitors choose to share and improve their experience.

For this popup creation guide, we'll use a template designed to collect feedback.

Step 2: Customize Your Popup Design

After you choose your template, the popup builder screen will show up. In the "Customize" step you add and edit elements, and in the "Style" step you can customize your layout's position, size, color palette & fonts to design your popup more suitable for your website.

The Customize step in the Popupsmart popup builder

You can edit a popup's image, text, and input fields according to your business as well.

A few customization decisions worth getting right at this stage:

  • CTA button copy: Specific action phrases ("Get the free guide") tell visitors what they get; generic ones ("Submit" or "Click here") don't.
  • Close button visibility: A clearly visible close option reduces frustration; don't hide it to force engagement.
  • Mobile preview: Switch to the mobile view in the editor before moving on. In Popupsmart's 2026 benchmark, 63% of popup displays happen on mobile. What looks good at desktop width can overlap critical content on smaller screens, which risks an SEO interstitial penalty; our guide to mobile popup design covers the layout rules.

Every element you add can be styled on its own. Click an element in one of the popup's blocks and change its style there.

The Style step in the Popupsmart popup builder

Step 3: Configure Targeting and Trigger Rules

When you have finished customizing your popup, go to the "Segment" section.

The Segment step in Popupsmart, where targeting and triggers are set

Targeting is where a no-code popup builder saves the most code: the rules engine handles behavior logic that would otherwise require custom JavaScript like the exit-intent hook above. Pick a predefined segment, or click "Edit Segment" and set:

  • Audience filters: Show the popup to new visitors only, returning visitors, specific traffic sources, or visitors from a particular country or device type.
  • Behavior triggers: Choose from exit intent (cursor moves toward the browser bar), scroll depth (fires after the visitor scrolls 50% or 75% of the page), time on page (fires after X seconds), or click triggers on a specific element. Popups that wait before appearing convert 0.83% vs 0.61% for popups shown the instant the page loads (median campaign); the direction holds in every year of data but the difference is not statistically reliable, so treat it as a lean, not a rule. Our guide to popup timing covers the delay options.
  • Frequency caps: Control how often the same visitor sees the popup: once per session, once per day, or a set number of times total. In Popupsmart's 2026 benchmark, the first two times a visitor sees a popup in a week convert at similar rates (1.33% and 1.22% of displays, pooled); from the third view conversion falls, and from the sixth view it is 0.41%, 31% of the first-view rate.
  • Page-level targeting: Restrict the popup to specific URLs or URL patterns with URL Targeting, so it only appears where it's relevant to the visitor's context.

There are different targeting options that you can choose according to your business objective.

By setting triggers on the "Audience," "User Behavior," and "Frequency Settings" parts, you decide exactly who sees the popup, and when.

When you are done with this part, click on the "Save" button and go to the "Settings" part.

Step 4: Connect Integrations and Publish

Before publishing, connect any tools you need in the Settings panel: email marketing tools (Mailchimp, Klaviyo, HubSpot, Brevo), CRMs, Google Analytics, and Zapier or a webhook for custom workflows. For lead-generation popups, connecting directly to your email platform means new subscribers land in your list without any manual export steps.

The integrations list in Popupsmart's campaign settings

Click on the "Publish" step when you are all set and ready to go!

When you finish creating your popup, add the embed code of your popup to your website's code injection part. You can easily add it before the closing body tag. In the dashboard's left bar, click Embed Code and copy the snippet. In a React app, paste it into the HTML page your app is served from (index.html in a Vite project), just before the closing </body> tag, outside the element React renders into, so it loads once and survives re-renders. Add the code only once per site. Besides, you can add popups with Google Tag Manager to your website.

Finally, add your domain under Websites and verify it, as the embed code guide in Popupsmart's help center shows. Popups only show on verified websites.

To open a Popupsmart campaign from a button in your React app, turn on On-Click Targeting under User Behavior in the campaign's Segment, copy the campaign id from the snippet it generates, and call window.ps.show() from a click handler:

declare global {
  interface Window {
    ps?: { show(options: { id: number }): void };
  }
}

// Copy the campaign id from the snippet Popupsmart generates under On-Click Targeting.
const CAMPAIGN_ID = 12345;

export default function PopupsmartButton() {
  return (
    <button type="button" onClick={() => window.ps?.show({ id: CAMPAIGN_ID })}>
      Get the newsletter
    </button>
  );
}

The optional chaining keeps the button harmless before the embed script has loaded. Popupsmart's help center advises against calling show() from links or buttons that navigate to another page.

Note
Quick Insight: React Router changes the URL without a page load. If a campaign uses URL Targeting, test it by navigating inside the app, not only by reloading the page.

The same steps work for an offer, an announcement or a feedback survey; only the template and the copy change.

Popupsmart integrates with many marketing tools and websites such as Mailchimp, HubSpot, Shopify, Brevo (formerly Sendinblue), and Squarespace; see the Popupsmart integrations for the full list.

Code gives you full control. A no-code builder is quicker to change and has the targeting built in, which suits a marketing team without a developer on call.

Wrap Up

React popups are built around a simple core, useState for visibility, <dialog> or portals for DOM placement, and event handlers for triggers, but getting them right requires attention to accessibility, performance, and mobile behavior. The four-step process in this guide covers the essentials: set up React 19, build the dialog component, customize it with CSS, and extend it to specific popup types.

The most important things to get right from the start: write an accessible component with ARIA attributes and focus handling, use the top layer or React portals to avoid z-index conflicts, and test on real mobile devices before shipping. Skipping any of these creates problems that are significantly harder to fix after launch.

If you'd rather skip the code entirely, Popupsmart lets you build, target, and publish popups through a visual editor, with no npm and no state management, and a free plan. It's a practical option for marketing teams who need to iterate quickly without engineering involvement.

FAQ about React Popups

What's the difference between a popup and a modal in React?

A popup is a lightweight overlay for quick information or contextual alerts, often positioned near a trigger element. A modal is a specific type of popup that takes over the full screen with a backdrop, demands user attention, and requires explicit dismissal. Modals always need focus trapping; simple popups may not. Use modals for critical decisions or forms, and lighter popups for supplementary information. The modal design best practices guide on UX Planet covers the decision criteria in detail.

How do I handle multiple popups on the same page without conflicts?

Managing multiple popups on one page requires centralized state to prevent two popups from opening simultaneously. Use React Context or a state management library like Redux to track which popup (if any) is currently open. Render each popup through a React portal to isolate its CSS and z-index from others. Assign unique keys to each popup component for predictable reconciliation. For testing, tools like the Storybook actions addon let you simulate multi-popup interactions in isolation.

Can I create a React popup with TypeScript?

Yes, and TypeScript adds significant value for popup components. Define interfaces for your popup's props; open state, close handler, children, and any content-specific props, to catch type mismatches at compile time rather than runtime. Most major popup libraries, including reactjs-popup, MUI, and Headless UI, ship with TypeScript definitions. Every component in this guide was type-checked with TypeScript 6.0. The TypeScript React handbook covers component typing patterns in depth.

How do I test React popups effectively?

Use Jest and React Testing Library for unit tests, verify the popup opens on trigger click, closes on the close button, and closes on Escape. Add ARIA attribute assertions to catch accessibility regressions. Use Cypress or Playwright for end-to-end keyboard navigation tests. Key edge cases: rapid open/close cycles and correct focus restoration after close. The React Testing Library docs cover accessible selector strategies.

How do React popups impact SEO?

Popups themselves don't harm SEO, but intrusive implementations do. Google's guidance on intrusive interstitials and dialogs says they make it hard for search engines to understand your content, which may lead to poor search performance. To stay safe: delay popup triggers past initial page render, ensure all page content is accessible to Googlebot regardless of popup state, and make dismissal obvious. Exit intent popups never fire for Google's crawler, which doesn't move a mouse. Our explainer on Google's rules for interstitial popups goes further.

What's the right way to handle popup state in a large React application?

In large apps, managing popup visibility as local component state doesn't scale. When multiple components need to trigger the same popup, or when popup content depends on global data (auth state, user profile, cart contents), lift the popup state to a Context provider or your existing state management solution. Create a single PopupManager context that tracks which popup is active and what data to display. This prevents duplicate popup instances and gives you a single place for analytics tracking.

Check out these blog posts as well:

Berna Partal

Growth Marketing Specialist

All posts

Berna is a Growth Marketer with 5+ years of experience in digital marketing, specializing in conversion rate optimization and SEO-driven content strategy. Her work focuses on growth marketing, data-driven content, and lifecycle automation to support lead generation. She holds a degree in English Language and Literature from Hacettepe University, strengthening her expertise in strategic storytelling, copywriting, and audience-focused communication. Her core expertise includes SEO, content strategy, marketing automation, and lead generation.

Reviewed by Mert Özeren