Next.js 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.
Offcanvas slides a panel in from an edge of the window. It suits filters or extra navigation that should not cover the whole page. On small screens, the AdminKit sidebar is one. The panel is Bootstrap's offcanvas, opened from React state instead of Bootstrap's JavaScript. It is in the free and PRO versions of AdminKit for Next.js.
As with the modal, the state lives in your code: show opens the panel and onHide asks to close it. In the App Router, pages are server components: they render on the server and have no state. So put the button, the state and the Offcanvas in a small client component, a file that starts with "use client". Then render that component from your page. Offcanvas itself is a client component too.
Example
"use client";
import { useState } from "react";
import { CloseButton } from "@/components/bootstrap/close-button";
import { Offcanvas } from "@/components/bootstrap/offcanvas";
export function OrderFilters() {
const [show, setShow] = useState(false);
const close = () => setShow(false);
return (
<>
<button
className="btn btn-primary"
type="button"
aria-controls="offcanvasFilters"
onClick={() => setShow(true)}
>
Filters
</button>
<Offcanvas
id="offcanvasFilters"
show={show}
onHide={close}
aria-labelledby="offcanvasFiltersLabel"
>
<div className="offcanvas-header">
<h2 className="offcanvas-title h5" id="offcanvasFiltersLabel">Filters</h2>
<CloseButton onClick={close} />
</div>
<div className="offcanvas-body">
<p>Show only the orders that match.</p>
<button type="button" className="btn btn-primary" onClick={close}>Apply</button>
</div>
</Offcanvas>
</>
);
}The page renders it like any component:
import { OrderFilters } from "@/components/order-filters";
import { pageMetadata } from "@/lib/site";
export const metadata = pageMetadata("/orders");
export default function OrdersPage() {
return (
<>
<h1 className="h3 mb-3">Orders</h1>
<OrderFilters />
</>
);
}pageMetadata() reads the route's title and description from src/data/pages.json, so a new page needs an entry there (see Add a page).
Server rendering
Unlike the modal, the panel stays in the DOM while it is hidden. So it is part of the HTML that Next.js builds for the page, hidden by .offcanvas.
The backdrop is different. It is rendered into document.body through a portal, and there is no document on the server. So the component renders the backdrop only once useMounted() (src/hooks/use-mounted.ts) says the page runs in the browser. The server HTML and the browser's first render then match. When React takes over the static HTML in the browser (hydration), it finds nothing that differs, so there is no hydration error.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
show | boolean | Whether the panel is open | |
onHide | () => void | Called on Escape and on a backdrop click; set show to false here | |
onShown | () => void | Called when the slide-in transition has ended | |
onHidden | () => void | Called when the slide-out transition has ended | |
placement | "start" | "end" | "top" | "bottom" | "start" | The edge it slides in from |
responsive | "sm" | "md" | "lg" | "xl" | "xxl" | A sheet only below this breakpoint, part of the page from it up | |
backdrop | boolean | "static" | true | true: a backdrop that closes the panel on click; "static": a backdrop that does not; false: none |
scroll | boolean | false | Keeps the page scrollable while the panel is open |
keyboard | boolean | true | Escape closes the panel |
as | ElementType | "div" | The element, such as "nav" or "aside" |
Other props, such as id, className, ref and aria-labelledby, go to the .offcanvas element. onHide, onShown and onHidden are functions: only a client component can pass them. The component is in src/components/bootstrap/offcanvas.tsx.
Placement
The panel comes in from the left unless you choose another edge. placement="end" adds .offcanvas-end and slides the panel in from the right. "top" and "bottom" use the other edges.
<Offcanvas
id="offcanvasActivity"
placement="end"
show={show}
onHide={close}
aria-labelledby="offcanvasActivityLabel"
>
<div className="offcanvas-header">
<h2 className="offcanvas-title h5" id="offcanvasActivityLabel">Activity</h2>
<CloseButton onClick={close} />
</div>
<div className="offcanvas-body">No new activity today.</div>
</Offcanvas>Backdrop and scrolling
The defaults are a backdrop and a locked page. When the user should keep using the page next to the panel, add scroll and backdrop={false}. The page then stays scrollable and usable:
<Offcanvas
id="offcanvasNotes"
scroll
backdrop={false}
show={show}
onHide={close}
aria-labelledby="offcanvasNotesLabel"
>
<div className="offcanvas-header">
<h2 className="offcanvas-title h5" id="offcanvasNotesLabel">Notes</h2>
<CloseButton onClick={close} />
</div>
<div className="offcanvas-body">Scroll the page: it stays scrollable while these notes are open.</div>
</Offcanvas>With backdrop="static", the backdrop is there but a click on it does nothing. Escape and your buttons still close the panel.
Responsive sidebar
The sidebar is a sheet on small screens and part of the page on large ones. responsive="lg" does that: it renders .offcanvas-lg, a sheet below the lg breakpoint and part of the page from lg up. src/components/sidebar/sidebar.tsx uses it with as="nav", and the navbar toggle opens it through the layout context.
<Offcanvas
as="nav"
id="sidebar"
responsive="lg"
show={sheetOpen}
onHide={() => setSheetOpen(false)}
aria-label="Sidebar"
>
...
</Offcanvas>Accessibility
- While open, the panel has
role="dialog"andaria-modal="true". Name it witharia-labelledbyoraria-label. - When it has slid in, the focus moves to the panel and Tab stays inside. On close, the focus returns to the element that opened it. A panel with
scrollandbackdrop={false}does not trap the focus, as in Bootstrap. - Escape closes it unless
keyboard={false}. CloseButtonhasaria-label="Close". Give the opening buttonaria-controlswith the panel'sid.
Related
- React offcanvas: the same component in AdminKit for React (Vite)
- Bootstrap 5 offcanvas: the offcanvas of the HTML version
- Modal: a dialog in the middle of the window
- Offcanvas demo: panels from every edge, with and without a backdrop
Last updated on
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.
PlaceholdersPro
Next.js Bootstrap placeholders in AdminKit PRO: loading cards in the static HTML, swapped for data fetched in a client component, with sizes and animation.