# 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.

> This component is only available in **AdminKit PRO**. [Learn more](/pricing/#nextjs).

`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](/nextjs/docs/components/accordion/) and the sidebar's nested menus are built on it. `Collapse` is part of the PRO version of [AdminKit for Next.js](/nextjs/).

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

```html
<button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#orderDetails" aria-expanded="false" aria-controls="orderDetails">
  Order details
</button>
<div class="collapse" id="orderDetails">
  <div class="card card-body mt-3">
    Ordered on May 4, paid by credit card, shipped to Amsterdam.
  </div>
</div>
```
```tsx code title="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:

```tsx title="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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `in` | `boolean` | | Required. Shown (`true`) or collapsed (`false`) |
| `as` | `ElementType` | `"div"` | The element to render, such as `"ul"` for a nested list |
| `onEntered` | `() => void` | | Called when the opening transition has ended |
| `onExited` | `() => void` | | Called 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`:

```html
<p>
  <button class="btn btn-primary me-2" type="button" data-bs-toggle="collapse" data-bs-target="#panelBilling" aria-expanded="false" aria-controls="panelBilling">Billing</button>
  <button class="btn btn-primary me-2" type="button" data-bs-toggle="collapse" data-bs-target="#panelShipping" aria-expanded="false" aria-controls="panelShipping">Shipping</button>
  <button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target=".panel-address" aria-expanded="false" aria-controls="panelBilling panelShipping">Both addresses</button>
</p>
<div class="row">
  <div class="col">
    <div class="collapse panel-address" id="panelBilling">
      <div class="card card-body">Billing: 12 Market Street, Amsterdam</div>
    </div>
  </div>
  <div class="col">
    <div class="collapse panel-address" id="panelShipping">
      <div class="card card-body">Shipping: 4 Harbor Road, Rotterdam</div>
    </div>
  </div>
</div>
```
```tsx code title="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.

## Related

- [React Bootstrap collapse](/react/docs/components/collapse/): the same component in AdminKit for React (Vite)
- [Bootstrap 5 collapse](/bootstrap/docs/components/collapse/): the collapse of the HTML version
- [Accordion](/nextjs/docs/components/accordion/): collapsing sections, one open at a time unless you set `alwaysOpen`
- [General UI demo](https://nextjs-demo.adminkit.io/ui-general): a card accordion built with `Collapse`

---

Canonical HTML page: https://adminkit.io/nextjs/docs/components/collapse/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
