AdminKitDocs
Components

Next.js Bootstrap Collapse

A Next.js Bootstrap collapse client component: a panel that opens and closes from React state with Bootstrap's height transition, rendered in the App Router.

Pro

This component is only available in AdminKit PRO. Learn more.

Collapse keeps secondary content out of the way until the user wants it, such as order details or a second address. A click on your toggle slides the panel open with Bootstrap's height animation. The accordion and the sidebar's nested menus are built on it. Collapse is part of the PRO version of AdminKit for Next.js.

It needs no Bootstrap JavaScript. A boolean prop switches the panel between Bootstrap's classes: .collapse when hidden, .collapsing while the height animates, and .collapse.show when open.

In the App Router, pages are server components: they render on the server and have no state. Collapse is a client component, from a file that starts with "use client". The open state and the button that changes it live in your code, so they need a client component too. Put the toggle and the panel in a small client component, and render that from your server component page.

Example

src/components/order-details.tsx
"use client";

import { useState } from "react";
import { Collapse } from "@/components/bootstrap/collapse";

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

  return (
    <>
      <button
        className="btn btn-primary"
        type="button"
        aria-expanded={open}
        aria-controls="orderDetails"
        onClick={() => setOpen(!open)}
      >
        Order details
      </button>
      <Collapse in={open} id="orderDetails">
        <div className="card card-body mt-3">
          Ordered on May 4, paid by credit card, shipped to Amsterdam.
        </div>
      </Collapse>
    </>
  );
}

Collapse always renders its children, so a closed panel is in the HTML that Next.js builds, hidden by .collapse (display: none). The browser's first render matches that HTML, and nothing animates when the page loads.

The panel content does not have to live in the client component. It can come from the server component, as children of your client component. The server renders it, and only the toggle logic runs in the browser:

src/components/details-toggle.tsx
"use client";

import { type ReactNode, useState } from "react";
import { Collapse } from "@/components/bootstrap/collapse";

export function DetailsToggle({ id, label, children }: { id: string; label: string; children: ReactNode }) {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button className="btn btn-light" type="button" aria-expanded={open} aria-controls={id} onClick={() => setOpen(!open)}>
        {label}
      </button>
      <Collapse in={open} id={id}>
        <div className="pt-3">{children}</div>
      </Collapse>
    </>
  );
}

Props

PropTypeDefaultDescription
inbooleanRequired. Shown (true) or collapsed (false)
asElementType"div"The element to render, such as "ul" for a nested list
onEntered() => voidCalled when the opening transition has ended
onExited() => voidCalled when the closing transition has ended

Other props, such as id, className, ref and aria-*, go to the collapsing element. Only changes of in after the first render animate. When the user prefers reduced motion, there is no transition, and onEntered and onExited run on the next frame. The component is in src/components/bootstrap/collapse.tsx.

Padding and margins

The height animation runs on the Collapse element itself, so padding or margins on it make the transition jump at the end. Put them on a child instead, as .card and mt-3 do above. It animates the height only: .collapse-horizontal does nothing.

Several panels

To open two panels from one button, derive that button's state from both. Every Collapse still has its own in:

src/components/addresses.tsx
"use client";

import { useState } from "react";
import { Collapse } from "@/components/bootstrap/collapse";

export function Addresses() {
  const [billing, setBilling] = useState(false);
  const [shipping, setShipping] = useState(false);
  const both = billing && shipping;

  return (
    <>
      <p>
        <button
          className="btn btn-primary me-2"
          type="button"
          aria-expanded={billing}
          aria-controls="panelBilling"
          onClick={() => setBilling(!billing)}
        >
          Billing
        </button>
        <button
          className="btn btn-primary me-2"
          type="button"
          aria-expanded={shipping}
          aria-controls="panelShipping"
          onClick={() => setShipping(!shipping)}
        >
          Shipping
        </button>
        <button
          className="btn btn-secondary"
          type="button"
          aria-expanded={both}
          aria-controls="panelBilling panelShipping"
          onClick={() => {
            setBilling(!both);
            setShipping(!both);
          }}
        >
          Both addresses
        </button>
      </p>
      <div className="row">
        <div className="col">
          <Collapse in={billing} id="panelBilling">
            <div className="card card-body">Billing: 12 Market Street, Amsterdam</div>
          </Collapse>
        </div>
        <div className="col">
          <Collapse in={shipping} id="panelShipping">
            <div className="card card-body">Shipping: 4 Harbor Road, Rotterdam</div>
          </Collapse>
        </div>
      </div>
    </>
  );
}

In the Bootstrap preview, "Both addresses" toggles each panel. The component above opens both, or closes both when both are open.

Accessibility

  • Collapse adds no ARIA attributes. Set aria-expanded and aria-controls on the toggle yourself.
  • Prefer a <button> as the toggle. A link toggle needs role="button" and event.preventDefault() in onClick.
  • While collapsed, the content is display: none: out of the tab order and skipped by screen readers.

Last updated on

On this page