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
"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):
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
| Prop | Type | Default | Description |
|---|---|---|---|
show | boolean | Whether the modal is open | |
onHide | () => void | Called on Escape and on a backdrop click; set show to false here | |
onExited | () => void | Called once the modal has faded out | |
backdrop | boolean | "static" | true | "static": a click outside shakes the dialog instead of closing it; false: no backdrop |
keyboard | boolean | true | Escape closes the modal |
dialogClassName | string | Classes for .modal-dialog: modal-lg, modal-dialog-centered, ... | |
contentClassName | string | Classes for .modal-content | |
contentAs | ElementType | "div" | "form" for a dialog that is a form |
contentProps | object | Other 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:
| Class | Effect |
|---|---|
modal-dialog-centered | Centers the dialog vertically |
modal-dialog-scrollable | Scrolls the body instead of the page when the content is long |
modal-sm, modal-lg, modal-xl | 400px, 900px and 1140px wide (600px by default) |
modal-fullscreen | Covers 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"andaria-modal="true". Give it a title and pointaria-labelledbyat the title'sid. - 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. CloseButtonhasaria-label="Close".
Related
- React Bootstrap modal: the same component in AdminKit for React (Vite)
- Bootstrap 5 modal: the modal of the HTML version
- Offcanvas: a panel that slides in from the side
- Modals demo: default, colored, centered and sized modals
Last updated on
List group
Next.js Bootstrap list group markup: static lists in server components, next/link items with the active route from usePathname, badges and cards.
Offcanvas
A Next.js offcanvas client component for the App Router: Bootstrap side panels in the static HTML, a backdrop added after hydration, four placements.