Skip to content

BbCollapsible

A region that animates open and closed from a boolean you own, with your own trigger.

import { BbCollapsible } from 'bitboss-ui';

On this page

Use it for

Reach for BbCollapsible when the trigger cannot be a header sitting over the body. A switch in a settings row, a "Show more" at the end of a paragraph, a button in a toolbar.

Use something else when

  • BbAccordion, if the trigger is a title over the body: it writes the button and the ARIA for you
  • BbSmoothHeight, if you hide nothing and only want a height change to animate
  • BbTabs, if they are peer views of one subject and only one stays visible

Pass Through

Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.

BbAccordion is this component plus a wired header button. Everything on this page is true there too.

Default

Bind a boolean and supply the control that flips it.

The component cannot open or close itself. It renders model-value and never writes it back, and it emits nothing. v-model compiles and works, but only as a prop binding, so do not wait for an update:modelValue. Put side effects on the trigger.

model-value is required. Initialize it with ref(false): an undefined binding renders as closed, which looks like the component ignoring you.

You own the trigger

The component manages the region. The semantics of the control that opens it are yours.

If the trigger is a button, give it aria-expanded and aria-controls pointing at the collapsible's id. Without them a screen-reader user gets a button whose effect is announced nowhere. That is what the Default demo above wires.

When the trigger is already a control with a state, do not add a second one:

A switch or a checkbox bound to the same ref already says "on / off", and the fields that appear are the consequence of the setting. Adding aria-expanded there announces the same fact twice.

If wiring this is not work you want, step up to BbAccordion: it writes the header button for you.

Closed content is out of reach

Hiding something visually is not hiding it. Here the component does that work.

Tab from the trigger while the panel is closed: focus skips straight past the button inside it. Requested: false

Closed content is aria-hidden and inert: unfocusable, unclickable, out of the tab order and out of the accessibility tree. Do not wrap it in a v-if: that would take the animation and the mounted state with it.

Nothing inside the slot renders until the first open, and it stays mounted from then on. eager renders it immediately. Reach for it when the content has to exist before anyone opens the region. A form field that must register and validate, text a prerendered page has to carry, anything measured on mount. The demo above is eager, which is why its text sits in this page's static HTML.

One region at a time

Several collapsibles become an exclusive group when you derive each state from one key.

Lumen Sit-Stand Desk 160

FUR-DSK-1187 · Furniture

Rated 4.8 / 5 · 12 units on hand · last updated 2026-08-21.

One ref holds the open key, each region compares against it, and each trigger assigns it. No group wrapper, and no watcher forcing siblings shut. Make the key nullable if clicking the open trigger should close everything.

When the triggers are stacked headers, you are describing an accordion: see One panel at a time.

Spacing and timing

Do not put padding, borders or gaps on the collapsible itself.

While closed they are forced to zero, so they show up only when it is open and the collapse leaks the difference. The inner wrapper, .bb-collapsible__content, is zeroed in both states. Space the element you slot in, as every demo on this page does.

transition-duration (milliseconds, 250 by default) drives the height animation and the opacity fade together. Under prefers-reduced-motion: reduce both drop to near-instant.

Coming from v2tag removed

The root is always a span laid out as a grid now. That is phrasing content, which is what keeps an inline "Show more" valid inside a paragraph. If you passed tag="section", wrap the component instead.

diff
- <BbCollapsible v-model="open" tag="section">…</BbCollapsible>
+ <section>
+   <BbCollapsible v-model="open">…</BbCollapsible>
+ </section>

Three classes are yours: .bb-collapsible on the root, with .bb-collapsible--open / .bb-collapsible--closed tracking the state, and .bb-collapsible__content on the wrapper around your slot. Style the state modifiers, not the animation: the height is a grid-template-rows transition between 0fr and 1fr, and overriding it is how you lose the collapse.