Edit Profile

Dark Mode

Copy config

Copy and paste the following code into your global.css file to apply the styles.

Collapsible

An interactive section that shows or hides its connected information.

✨ Features

  • Accessible as a button that shows content, following web a11y standards.
  • Full keyboard navigation
  • Controlled or uncontrolled
  • Initial open state does not wake up the component
  • Automatic entry/exit animation detection
  • Executes on interaction or programmatically

Building blocks

import { component$ } from '@builder.io/qwik';
import { Collapsible } from '@qwik-ui/headless';

export default component$(() => (
  <Collapsible.Root>
    <Collapsible.Trigger>Button</Collapsible.Trigger>
    <Collapsible.Content>Content</Collapsible.Content>
  </Collapsible.Root>
));

🎨 Anatomy

ComponentDescription
Collapsible.Root

The root container for the Collapsible component.

Collapsible.Trigger

A button that opens the Collapsible content when interacted with.

Collapsible.Content

Contains the content associated with a Collapsible.

Why use a headless collapsible?

One of the most common questions: "why not use the native details and summary HTML elements?".

As much as we love the native elements, they come with a couple of problems:

Native element pain points

  • Inconsistent accessibility and browser support
  • Inconsistent screen reader support
  • Hierarchy and DOM structure restrictions
  • Lack of full programmatic control

Component State

Uncontrolled / Initial value

We can select an initial uncontrolled value by passing the open prop to the <Collapsible.Root /> component.

Content

The above example expands the collapsible by default. Something to notice, is there isn't any layout shift when refreshing the page.

This is because the content is rendered on the server. Animations applied to data-open take effect after the initial render to prevent layout shift.

Controlled / Reactive value

We can pass reactive state by using the bind:open prop to the <Collapsible.Root /> component.

is open: false

bind:open is a signal prop, and allows us to pass in our own signal to control the expanded state of the collapsible.

Programmatic changes

Now that we have a controlled state, we can programmatically change the state of the collapsible by changing the value of the signal.

is open: false

The example above toggles the expanded state of the collapsible by changing the value of the isOpen signal when the checkbox is clicked.

Handling open / close

We may want to handle the open / close of the collapsible. For example, we may want execute some code when the collapsible is opened or closed.

count: 0

To do that, we can use the onChange$ prop. A parameter is passed to the handler, which is a boolean indicating whether the collapsible is open or closed.

Disabled collapsible

The collapsible can be disabled by adding the disabled prop to the <Collapsible.Root /> component.

Animating the content

To animate the height of the content, we can use a keyframe animation on the height property.

Content

Height Animation

To animate the Accordion content, the --qwikui-collapsible-content-height CSS variable in your keyframes.

Content

.collapsible-content {
  overflow: hidden;
}

.collapsible-content[data-open] {
  animation: 550ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards collapsible-open;
}
.collapsible-content[data-closed] {
  animation: 350ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards collapsible-closed;
}

@keyframes collapsible-open {
  from {
    height: 0;
  }
  to {
    height: var(--qwikui-collapsible-content-height);
  }
}

@keyframes collapsible-closed {
  from {
    height: var(--qwikui-collapsible-content-height);
  }
  to {
    height: 0;
  }
}

Why does padding or border break the animation?

Padding or border applied to Collapsible.Content breaks our keyframe animation above. This is because the content height has changed.

To fix this, add a child element to the content, and set the padding or border on that element.

<Collapsible.Content class="collapsible-animation collapsible-content">
  <p class="collapsible-content-outline">Content</p>
</Collapsible.Content>

CSR

The collapsible can be rendered both server-side or client-side, same with the rest of the components.

The main difference, is there is no server to client handoff. This can be useful if you're navigating via SPA.

Example CSS

.collapsible {
  min-width: 14rem;
}

.collapsible-trigger {
  width: 100%;
  border: 2px dotted hsla(var(--foreground) / 1);
  border-radius: calc(var(--border-radius) / 2);
  padding: 0.5rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.collapsible-trigger[data-disabled] {
  opacity: 0.3;
}

.collapsible-trigger:hover {
  background-color: hsla(var(--primary) / 0.08);
}

.collapsible-trigger svg {
  width: 1.25rem;
  height: 1.25rem;
}

.collapsible-trigger[data-open] {
  border-bottom: none;
}

.collapsible-trigger[data-open] svg {
  transform: rotate(180deg);
}

.collapsible-content {
  width: 100%;
  font-weight: 500;
  background: hsla(var(--primary) / 0.2);
  border-radius: calc(var(--border-radius) / 2);
  max-width: var(--select-width);
  color: hsl(var(--foreground));
  overflow: hidden;
  margin-top: -2px;
}

.collapsible:has(.collapsible-content:not([hidden])) .collapsible-trigger {
  border-bottom: 2px dotted;
}

.collapsible-content-outline {
  padding: 0.5rem;
  border: 2px dotted hsla(var(--primary) / 1);
}

/* animations only */
.collapsible-transition {
  transition: transform 500ms ease;
}

@keyframes collapsible-open {
  0% {
    height: 0;
    margin-top: 0;
  }
  100% {
    height: var(--qwikui-collapsible-content-height);
    margin-top: -2px;
  }
}

@keyframes collapsible-closed {
  0% {
    height: var(--qwikui-collapsible-content-height);
    margin-top: -2px;
  }
  100% {
    height: 0;
    margin-top: 0;
  }
}

.collapsible-animation[data-open] {
  animation: 550ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards collapsible-open;
}

.collapsible-animation[data-closed] {
  animation: 350ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards collapsible-closed;
}

Every code example uses the following CSS:

Some CSS variables are specific to the docs, feel free to plug in your own values or variables!

Keyboard Interaction

Key

Description

Space
When focus is on the trigger, open or close the collapsible.
Enter
When focus is on the trigger open or close the collapsible.

API

Data Attributes

Collapsible.Root, Collapsible.Trigger, and Collapsible.Content all have the following data attributes that are used to track state:

AttributeDescription
data-open

If the collapsible is open (Boolean).

data-closed

If the collapsible is closed (Boolean).

data-disabled

If the collapsible is disabled (Boolean).

Collapsible.Root

PropTypeDescription
open
boolean

Uncontrolled initial expanded value.

bind:open
signal

Controlled expanded value, manages the collapsible content.

onOpenChange$
QRL

Function called when the collapsible opens or closes.

disabled
boolean

Disables the collapsible when true.