Collapse

Material Design 3-inspired expandable/collapsible component with smooth animations

Collapse

Expand And Collapse Animation

Native details.collapse animates its content height in browsers supporting ::details-content and interpolate-size: allow-keywords. Controlled panels animate when the application synchronizes .collapse-open/.show, hidden, inert, and ARIA. Set inert immediately on closing and return focus to the trigger if focus was inside. Discrete display transitions retain the closing visual until the height transition finishes, without retaining keyboard access. Reduced motion and older browsers use immediate disclosure. CSS does not update ARIA or manage focus.

Animated native disclosure

Delivery details

Track your delivery and review its destination.

Review delivery

Native And Controlled State

Native Collapse

Shipping information

Ships in two days.

details[open] is the native authority, even when compatibility classes conflict. Native exclusive groups can share name; unsupported browsers remain independent. Applications own any required exclusive-group fallback. Independent items omit name.

For non-details markup, .collapse-closed wins over .collapse-open and legacy .show. Remove contradictory classes in your controller. Fade, slide and horizontal variants use the same state contract; no outer item opens a nested item. CSS does not synchronize accessibility or focus.

<div class="collapse collapse-closed" id="demo-controlled-collapse">
  <button class="collapse-trigger" aria-expanded="false" aria-controls="demo-collapse-panel">Shipping</button>
  <div class="collapse-content" id="demo-collapse-panel" hidden inert><p>Ships in two days.</p></div>
</div>
function setExpanded(item, expanded) {
  const trigger = item.querySelector(':scope > .collapse-trigger');
  const panel = item.querySelector(':scope > .collapse-content');
  if (!expanded && panel.contains(document.activeElement)) trigger.focus();
  trigger.setAttribute('aria-expanded', String(expanded));
  item.classList.toggle('collapse-open', expanded);
  item.classList.toggle('collapse-closed', !expanded);
  item.classList.remove('show');
  panel.hidden = !expanded;
  panel.inert = !expanded;
}
const item = document.getElementById('demo-controlled-collapse');
item.querySelector(':scope > .collapse-trigger').addEventListener('click', () => {
  const expanded = item.querySelector(':scope > .collapse-trigger').getAttribute('aria-expanded') !== 'true';
  setExpanded(item, expanded);
});

Bind semantic button clicks to this setter. Controlled exclusive groups close direct siblings through the same setter. Initialize closed panels hidden/inert and restore focus before hiding an active descendant; CSS clipping alone is not sufficient. The style demos below use native disclosure; framework examples implement the controlled contract.

The Collapse component allows users to show and hide content sections with smooth animations. It’s perfect for FAQs, navigation menus, and content-heavy interfaces where space management is crucial.

Basic Usage

Basic Collapse

Click to toggle ▼

This is the collapsible content that can be shown or hidden.

States

Open State

Native disclosure uses the open attribute. Controlled markup uses collapse-open with the controller contract above:

Open State

Expanded Section ▼

This content is visible by default.

Closed State

Omit open for a closed native disclosure. Controlled markup uses collapse-closed and hidden/inert:

Closed State

Collapsed Section ▼

This content is hidden by default.

Variants

Bordered Variant

Add a border to the collapse component:

Bordered Variant

Bordered Collapse ▼

Content with a border around it.

Card Variant

Card variant with shadow and border:

Card Variant

Card Style Collapse ▼

This collapse has a card appearance with shadow.

Ghost Variant

Minimal styling for a clean look:

Ghost Variant

Ghost Collapse ▼

Minimal styling with transparent background.

Color Variants

Primary

Primary Color

Primary Collapse ▼

Content with primary color theme.

Secondary

Secondary Color

Secondary Collapse ▼

Content with secondary color theme.

Tertiary

Tertiary Color

Tertiary Collapse ▼

Content with tertiary color theme.

Sizes

Small

Small Size

Small Collapse ▼

Compact size for space-constrained layouts.

Medium (Default)

Medium Size (Default)

Medium Collapse (Default) ▼

Standard size for most use cases.

Large

Large Size

Large Collapse ▼

Larger size for emphasis and better touch targets.

Accordion Groups

Create an exclusive native group with a shared name. Older browsers without grouped-details support remain independent; an application can provide the fallback:

Accordion Group

Section 1 ▼

First section content is expanded by default.

Section 2 ▼

Second section content is collapsed.

Section 3 ▼

Third section content is collapsed.

Animation Variants

Fade Animation

Smooth fade in/out effect:

Fade Animation

Fade Animation ▼

Content fades in and out smoothly.

Slide Animation

Slide with transform effect:

Slide Animation

Slide Animation ▼

Content slides in from above with a transform.

Additional Features

With Divider

Add a divider between trigger and content:

With Divider

With Divider ▼

Content separated by a divider line.

Disabled State

Prevent interaction with the collapse:

Disabled State

Loading State

Show a loading indicator:

Loading State

Loading Content ▼

Content is being loaded...

Nested Collapse

Collapse components can be nested within each other:

Nested Collapse

Parent Section ▼

Parent content with nested collapse below:

Nested Section ▼

Nested collapse content with reduced padding.

JavaScript Integration

This controlled example runs an application controller. The complete setter in Native And Controlled State synchronizes classes, ARIA, hidden/inert and focus; importing Collapse CSS does not attach handlers.

Best Practices

Accessibility

  • Use native <summary> triggers, or semantic <button> triggers with an application controller
  • Provide clear, descriptive labels
  • Ensure keyboard navigation support (Enter/Space to toggle)
  • Native details supplies disclosure semantics; controlled triggers must synchronize ARIA

Accessible Collapse

Accessible Collapse

Content with proper ARIA attributes.

Performance

  • Keep content lightweight for smooth animations
  • Avoid heavy computations during expand/collapse
  • Use CSS transforms for better performance
  • Consider lazy loading content for large datasets

UX Guidelines

  1. Clear Visual Feedback: Use icons to indicate expanded/collapsed state
  2. Smooth Animations: Default 300ms ease-in-out provides good balance
  3. Touch Targets: Ensure triggers are at least 44×44px for mobile
  4. Context Preservation: Consider scroll position when toggling

Framework Examples

React

import { useId, useState } from 'react';

interface CollapseProps {
  title: string;
  children: React.ReactNode;
  defaultOpen?: boolean;
  variant?: 'default' | 'bordered' | 'card' | 'ghost';
  color?: 'default' | 'primary' | 'secondary' | 'tertiary';
  size?: 'sm' | 'md' | 'lg';
}

export function Collapse({
  title,
  children,
  defaultOpen = false,
  variant = 'default',
  color = 'default',
  size = 'md'
}: CollapseProps) {
  const [isOpen, setIsOpen] = useState(defaultOpen);
  const panelId = useId();

  const classes = [
    'collapse',
    isOpen ? 'collapse-open' : 'collapse-closed',
    variant !== 'default' ? `collapse-${variant}` : '',
    color !== 'default' ? `collapse-${color}` : '',
    size !== 'md' ? `collapse-${size}` : ''
  ].filter(Boolean).join(' ');

  return (
    <div className={classes}>
      <button
        className="collapse-trigger"
        onClick={() => setIsOpen(!isOpen)}
        aria-expanded={isOpen}
        aria-controls={panelId}
      >
        <span>{title}</span>
        <span className="collapse-icon">▼</span>
      </button>
      <div className="collapse-content" id={panelId} hidden={!isOpen} inert={!isOpen}>
        {children}
      </div>
    </div>
  );
}

// Usage
<Collapse title="FAQ Item" variant="bordered" color="primary">
  <p>Answer to the frequently asked question.</p>
</Collapse>

Vue

<template>
  <div :class="collapseClasses">
    <button
      class="collapse-trigger"
      @click="toggle"
      :aria-expanded="isOpen"
      :aria-controls="panelId"
    >
      <span>{{ title }}</span>
      <span class="collapse-icon">▼</span>
    </button>
    <div class="collapse-content" :id="panelId" :hidden="!isOpen" :inert="!isOpen">
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, useId } from 'vue';

interface Props {
  title: string;
  defaultOpen?: boolean;
  variant?: 'default' | 'bordered' | 'card' | 'ghost';
  color?: 'default' | 'primary' | 'secondary' | 'tertiary';
  size?: 'sm' | 'md' | 'lg';
}

const props = withDefaults(defineProps<Props>(), {
  defaultOpen: false,
  variant: 'default',
  color: 'default',
  size: 'md'
});

const isOpen = ref(props.defaultOpen);
const panelId = useId(); // Vue 3.5+; older applications must supply a unique ID.

const collapseClasses = computed(() => [
  'collapse',
  isOpen.value ? 'collapse-open' : 'collapse-closed',
  props.variant !== 'default' ? `collapse-${props.variant}` : '',
  props.color !== 'default' ? `collapse-${props.color}` : '',
  props.size !== 'md' ? `collapse-${props.size}` : ''
].filter(Boolean).join(' '));

const toggle = () => {
  isOpen.value = !isOpen.value;
};
</script>

<!-- Usage -->
<Collapse title="FAQ Item" variant="bordered" color="primary">
  <p>Answer to the frequently asked question.</p>
</Collapse>

Svelte

<script lang="ts">
  export let title: string;
  export let panelId: string; // Supply a unique ID per instance.
  export let defaultOpen = false;
  export let variant: 'default' | 'bordered' | 'card' | 'ghost' = 'default';
  export let color: 'default' | 'primary' | 'secondary' | 'tertiary' = 'default';
  export let size: 'sm' | 'md' | 'lg' = 'md';

  let isOpen = defaultOpen;

  $: collapseClasses = [
    'collapse',
    isOpen ? 'collapse-open' : 'collapse-closed',
    variant !== 'default' ? `collapse-${variant}` : '',
    color !== 'default' ? `collapse-${color}` : '',
    size !== 'md' ? `collapse-${size}` : ''
  ].filter(Boolean).join(' ');
</script>

<div class={collapseClasses}>
  <button
    class="collapse-trigger"
    on:click={() => isOpen = !isOpen}
    aria-expanded={isOpen}
    aria-controls={panelId}
  >
    <span>{title}</span>
    <span class="collapse-icon">▼</span>
  </button>
  <div class="collapse-content" id={panelId} hidden={!isOpen} inert={!isOpen}>
    <slot />
  </div>
</div>

<!-- Usage -->
<Collapse panelId="demo-svelte-faq-panel" title="FAQ Item" variant="bordered" color="primary">
  <p>Answer to the frequently asked question.</p>
</Collapse>

API Reference

Class Names

Class Description
.collapse Base collapse container (required)
.collapse-open Expanded state - shows content
.collapse-closed Collapsed state - hides content
.collapse-trigger Clickable trigger/header element
.collapse-icon Icon that rotates on toggle
.collapse-content Content area that expands/collapses
.collapse-bordered Adds border around collapse
.collapse-card Card style with shadow and border
.collapse-ghost Minimal styling, transparent background
.collapse-primary Primary color theme
.collapse-secondary Secondary color theme
.collapse-tertiary Tertiary color theme
.collapse-sm Small size variant
.collapse-lg Large size variant
.collapse-group Container for accordion groups
.collapse-fade Fade animation effect
.collapse-slide Slide animation with transform
.collapse-divider Adds divider between trigger and content
.collapse-disabled Disables interaction
.collapse-loading Shows loading spinner

Animation Details

The collapse component uses the following animation timing:

  • Duration: 300ms
  • Easing: ease-in-out
  • Icon Rotation: 180deg on expand
  • Fade Transition: Opacity changes over 200ms
  • Slide Transform: translateY(-0.5rem) when collapsed

Combinations

Combine classes for customized appearance:

Combined Variants

Custom Collapse ▼

Combined styling options.

Enhanced Collapse ▼

Multiple variant combinations.

  • Accordion - Multiple collapse components working together
  • Card - Container component
  • List - List items with expandable content
  • Tabs - Alternative content organization

See Also