How to Trigger a Bootstrap Modal in React Without npm or React-Bootstrap

You can trigger a bootstrap modal react implementation by using ReactDOM.createPortal to render the modal markup outside the component tree and imperatively controlling it via the global bootstrap.Modal API loaded from a CDN.

This approach leverages React’s portal system—implemented in the facebook/react repository—to mount DOM nodes outside the normal hierarchy while preserving React’s lifecycle management. By combining vanilla Bootstrap’s JavaScript behavior with React portals, you avoid adding npm dependencies like react-bootstrap while maintaining full modal functionality including backdrop handling, keyboard dismissal, and focus management.

Why Use Bootstrap Modal React Without npm Packages

Bootstrap’s modal requires two specific ingredients to function: the HTML markup structure and the JavaScript behavior that toggles visibility. In standard HTML, you would include Bootstrap’s CSS and JS bundles, then instantiate new bootstrap.Modal(element).show().

In React, the challenge is that modals must live outside the normal component tree to avoid CSS clipping and z-index conflicts from parent containers. The solution is to use React portals, which allow components to render into a different part of the DOM while maintaining their place in the React component hierarchy.

The React Portal Solution for Bootstrap Modals

React implements portals through ReactDOM.createPortal, defined in packages/react-dom/src/shared/ReactDOM.js. This function forwards to the reconciler implementation in packages/react-reconciler/src/ReactPortal.js, which creates a separate fiber tree for the portal’s children while maintaining event bubbling through the React tree.

By rendering your modal markup into document.body via a portal, you achieve a self-contained dialog that behaves exactly like vanilla Bootstrap, unconstrained by parent component CSS.

Step-by-Step Implementation

1. Load Bootstrap Assets via CDN

Add the Bootstrap CSS and JavaScript bundles to your HTML template. This provides the global bootstrap namespace without requiring npm installation.

<!-- public/index.html -->
<link
  href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
  rel="stylesheet"
/>
<script
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
  defer
></script>

2. Create the Portal Component

Build a reusable component that uses ReactDOM.createPortal to mount the modal outside the normal React tree. Store a reference to the modal DOM node for imperative control.

// ModalPortal.tsx
import { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';

type Props = {
  children: React.ReactNode;
  isOpen: boolean;
  onClose: () => void;
};

export default function ModalPortal({ children, isOpen, onClose }: Props) {
  const modalRef = useRef<HTMLDivElement>(null);
  const bsModalRef = useRef<any>(null);

  useEffect(() => {
    if (modalRef.current && (window as any).bootstrap) {
      const { Modal } = (window as any).bootstrap;
      
      bsModalRef.current = new Modal(modalRef.current, {
        backdrop: true,
        keyboard: true,
      });

      // Sync React state when Bootstrap hides the modal
      modalRef.current.addEventListener('hidden.bs.modal', onClose);
    }

    return () => {
      if (bsModalRef.current) {
        bsModalRef.current.dispose();
      }
    };
  }, []);

  useEffect(() => {
    if (bsModalRef.current) {
      isOpen ? bsModalRef.current.show() : bsModalRef.current.hide();
    }
  }, [isOpen]);

  return ReactDOM.createPortal(
    <div className="modal fade" tabIndex={-1} ref={modalRef}>
      <div className="modal-dialog">
        <div className="modal-content">{children}</div>
      </div>
    </div>,
    document.body
  );
}

3. Instantiate the Bootstrap Modal API

The useEffect hook initializes new bootstrap.Modal(element) only after the DOM node exists. This imperative instantiation connects React’s declarative rendering with Bootstrap’s JavaScript behavior, enabling features like backdrop clicking and keyboard dismissal.

4. Handle Cleanup and Events

Always dispose of the Bootstrap modal instance when the component unmounts to prevent memory leaks. Listen to Bootstrap’s hidden.bs.modal event to synchronize React’s state when the user dismisses the modal via backdrop click or ESC key.

Complete Working Example

Here is a complete implementation showing how to use the portal component within an application:

// App.tsx
import { useState } from 'react';
import ModalPortal from './ModalPortal';

export default function App() {
  const [isModalOpen, setModalOpen] = useState(false);

  return (
    <div className="container mt-5">
      <button 
        className="btn btn-primary" 
        onClick={() => setModalOpen(true)}
      >
        Launch Bootstrap Modal
      </button>

      <ModalPortal
        isOpen={isModalOpen}
        onClose={() => setModalOpen(false)}
      >
        <div className="modal-header">
          <h5 className="modal-title">Bootstrap Modal in React</h5>
          <button 
            type="button" 
            className="btn-close" 
            onClick={() => setModalOpen(false)}
          />
        </div>
        <div className="modal-body">
          <p>This modal uses React portals and vanilla Bootstrap JavaScript without any npm packages.</p>
        </div>
        <div className="modal-footer">
          <button 
            className="btn btn-secondary" 
            onClick={() => setModalOpen(false)}
          >
            Close
          </button>
        </div>
      </ModalPortal>
    </div>
  );
}

How React Portals Work Under the Hood

According to the facebook/react source code, portals are implemented through two key files:

  • packages/react-dom/src/shared/ReactDOM.js – This file exports the createPortal function that developers call. It serves as the entry point for portal creation in the ReactDOM package.

  • packages/react-reconciler/src/ReactPortal.js – This file contains the reconciler implementation that handles portal logic. It creates a separate fiber tree for the portal’s children while maintaining event bubbling through the original React component tree, ensuring that events from the modal (like button clicks) still propagate to parent components in React’s virtual DOM.

  • packages/react-dom/src/client/ReactDOMClient.js – This file demonstrates how React mounts into DOM containers, which is relevant for understanding how the portal target (such as document.body) integrates with the overall render tree.

This architecture explains why portals are the ideal solution for bootstrap modal react implementations: they render DOM nodes outside the normal component hierarchy (avoiding CSS constraints) while preserving React’s event system and lifecycle management.

Summary

  • Use ReactDOM.createPortal to render modal markup into document.body, avoiding CSS clipping from parent containers.
  • Load Bootstrap via CDN to access the global bootstrap.Modal API without npm packages.
  • Instantiate imperatively using new bootstrap.Modal(element) in a useEffect hook to connect React’s declarative rendering with Bootstrap’s JavaScript behavior.
  • Handle cleanup by calling dispose() on the Bootstrap instance when the component unmounts to prevent memory leaks.
  • Synchronize state by listening to Bootstrap’s hidden.bs.modal event to update React state when users dismiss the modal via backdrop or keyboard.

Frequently Asked Questions

Can I use bootstrap modal react without installing any npm packages?

Yes. By loading Bootstrap’s CSS and JavaScript from a CDN in your HTML template, you gain access to the global bootstrap object. You can then use React’s portal system to render modal markup and control it via the vanilla Bootstrap Modal API, eliminating the need for react-bootstrap or any other npm dependency.

Why use ReactDOM.createPortal instead of rendering the modal inline?

Rendering a modal inline within your component tree often causes CSS issues such as overflow: hidden clipping or z-index stacking problems from parent containers. ReactDOM.createPortal renders the DOM nodes into a different container (typically document.body) while keeping the component in the React tree, ensuring events bubble correctly and the modal displays above all other content.

How do I handle the Bootstrap modal events in React?

Bootstrap fires custom events like shown.bs.modal and hidden.bs.modal when the modal opens or closes. In your useEffect hook, add event listeners to the modal DOM element to synchronize React state. For example, listen for hidden.bs.modal to update your isOpen state when the user clicks the backdrop or presses ESC, ensuring React and Bootstrap stay in sync.

Is this approach compatible with Bootstrap 4 and Bootstrap 5?

Yes, this approach works with both Bootstrap 4 and Bootstrap 5. The main difference is the global namespace: Bootstrap 5 uses bootstrap.Modal while Bootstrap 4 uses jQuery or the global Modal depending on your build. When using the CDN bundle as shown in the examples, Bootstrap 5 attaches to window.bootstrap, making the imperative instantiation straightforward for modern React applications.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →