AdminKitDocs
Components

Next.js Bootstrap Modal

A Next.js Bootstrap modal for the App Router: a client component opened from state in a server component page, with static, centered and fullscreen variants.

A modal asks for the user's attention before they go on: to confirm a delete, fill in a short form or read an important message. Modal gives your Next.js pages Bootstrap's dialog, with its look and fade, opened from React state instead of Bootstrap's JavaScript. It is in the free and PRO versions of AdminKit for Next.js.

In the App Router, pages are server components: they render on the server and have no state. A modal needs state to know whether it is open, so it goes in a small client component, a file that starts with "use client". Put the button, the state and the Modal there, and render that component from your page. The page itself stays a server component and keeps its metadata export.

Example

src/components/delete-project.tsx
"use client";

import { useState } from "react";
import { CloseButton } from "@/components/bootstrap/close-button";
import { Modal } from "@/components/bootstrap/modal";

export function DeleteProject() {
  const [show, setShow] = useState(false);
  const close = () => setShow(false);

  return (
    <>
      <button type="button" className="btn btn-danger" onClick={() => setShow(true)}>
        Delete project
      </button>
      <Modal show={show} onHide={close} aria-labelledby="delete-title">
        <div className="modal-header">
          <h2 className="modal-title h5" id="delete-title">Delete project</h2>
          <CloseButton onClick={close} />
        </div>
        <div className="modal-body">This cannot be undone.</div>
        <div className="modal-footer">
          <button type="button" className="btn btn-secondary" onClick={close}>Cancel</button>
          <button type="button" className="btn btn-danger" onClick={close}>Delete</button>
        </div>
      </Modal>
    </>
  );
}

The page imports it like any other component. pageMetadata() reads the route from src/data/pages.json, so a new page needs an entry there (see Add a page):

src/app/(dashboard)/projects/page.tsx
import { DeleteProject } from "@/components/delete-project";
import { pageMetadata } from "@/lib/site";

export const metadata = pageMetadata("/projects");

export default function ProjectsPage() {
  return (
    <>
      <h1 className="h3 mb-3">Projects</h1>
      <DeleteProject />
    </>
  );
}

The modal only exists in the browser. The HTML that Next.js builds for the page has the button but not the dialog, so the server and browser output never disagree. When it opens, the dialog is added at the end of <body>.

Props

PropTypeDefaultDescription
showbooleanWhether the modal is open
onHide() => voidCalled on Escape and on a backdrop click; set show to false here
onExited() => voidCalled once the modal has faded out
backdropboolean | "static"true"static": a click outside shakes the dialog instead of closing it; false: no backdrop
keyboardbooleantrueEscape closes the modal
dialogClassNamestringClasses for .modal-dialog: modal-lg, modal-dialog-centered, ...
contentClassNamestringClasses for .modal-content
contentAsElementType"div""form" for a dialog that is a form
contentPropsobjectOther props for .modal-content, such as onSubmit

Other props, such as className, id and aria-labelledby, go to the .modal element. A server component cannot pass a function, so the component that passes onHide is a client component too. The modal's code is in src/components/bootstrap/modal.tsx.

Static backdrop

When the user must make a choice first, set backdrop="static" and keyboard={false}. A click next to the dialog or Escape then makes it bounce instead of closing.

<Modal
  show={show}
  onHide={close}
  backdrop="static"
  keyboard={false}
  aria-labelledby="unsaved-title"
>
  <div className="modal-header">
    <h2 className="modal-title h5" id="unsaved-title">Unsaved changes</h2>
  </div>
  <div className="modal-body">Save your changes before you leave this page?</div>
  <div className="modal-footer">
    <button type="button" className="btn btn-secondary" onClick={discard}>Discard</button>
    <button type="button" className="btn btn-primary" onClick={save}>Save</button>
  </div>
</Modal>

Centered, scrollable and sizes

dialogClassName takes Bootstrap's .modal-dialog classes:

ClassEffect
modal-dialog-centeredCenters the dialog vertically
modal-dialog-scrollableScrolls the body instead of the page when the content is long
modal-sm, modal-lg, modal-xl400px, 900px and 1140px wide (600px by default)
modal-fullscreenCovers the whole window; modal-fullscreen-md-down only below a breakpoint
<Modal
  show={show}
  onHide={close}
  dialogClassName="modal-dialog-centered modal-lg"
  aria-labelledby="invite-title"
>
  <div className="modal-header">
    <h2 className="modal-title h5" id="invite-title">Invite members</h2>
    <CloseButton onClick={close} />
  </div>
  <div className="modal-body">A large, vertically centered dialog.</div>
</Modal>

className="modal-colored modal-primary" (or another theme color) gives the modal a colored background, as in the demo.

Form in a modal

AdminKit for Next.js builds to static files (output: "export"), so there is no server to run Server Actions. A form in a modal sends its data from the browser instead, for example with fetch to your API. Make the modal itself the form with contentAs="form":

<Modal
  show={show}
  onHide={close}
  contentAs="form"
  contentProps={{
    onSubmit: async (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      await fetch("/api/projects/rename", {
        method: "POST",
        body: new FormData(event.currentTarget),
      });
      close();
    },
  }}
  aria-labelledby="rename-title"
>
  <div className="modal-header">
    <h2 className="modal-title h5" id="rename-title">Rename project</h2>
    <CloseButton onClick={close} />
  </div>
  <div className="modal-body">
    <label htmlFor="project-name" className="form-label">Name</label>
    <input id="project-name" name="name" className="form-control" required />
  </div>
  <div className="modal-footer">
    <button type="submit" className="btn btn-primary">Save</button>
  </div>
</Modal>

Accessibility

  • The modal has role="dialog" and aria-modal="true". Give it a title and point aria-labelledby at the title's id.
  • While it is open, the focus stays inside the modal. When it closes, the focus returns to the element that opened it.
  • Escape closes the modal, unless keyboard={false}. Then keep a visible button that closes it.
  • CloseButton has aria-label="Close".

Last updated on

On this page