Skip to content
Esc
navigateopen⌘Jpreview
On this page

Collapsible

An interactive component which expands and collapses a panel.

@peduarte starred 3 repositories
@radix-ui/primitives
@base-ui/react
@stylexjs/stylex
import * as stylex from '@stylexjs/stylex';

import { Button } from '@/components/ui/button';
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { space, fontSize, fontWeight, stroke, container } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

export default function CollapsibleDemo() {
  return (
    <Collapsible defaultOpen {...stylex.props(styles.root)}>
      <div {...stylex.props(styles.header)}>
        <span {...stylex.props(styles.title)}>
          @peduarte starred 3 repositories
        </span>
        <CollapsibleTrigger render={<Button variant="ghost" size="sm" />}>
          Toggle
        </CollapsibleTrigger>
      </div>
      <div {...stylex.props(styles.repo)}>@radix-ui/primitives</div>
      <CollapsibleContent style={styles.panel}>
        <div {...stylex.props(styles.repo)}>@base-ui/react</div>
        <div {...stylex.props(styles.repo)}>@stylexjs/stylex</div>
      </CollapsibleContent>
    </Collapsible>
  );
}

const styles = stylex.create({
  root: {
    display: 'flex',
    flexDirection: 'column',
    fontFamily: font.sans,
    gap: space.s2,
    width: container.sm,
  },
  header: {
    alignItems: 'center',
    display: 'flex',
    justifyContent: 'space-between',
  },
  title: {
    fontSize: fontSize.sm,
    fontWeight: fontWeight.medium,
  },
  panel: {
    display: 'flex',
    flexDirection: 'column',
    gap: space.s2,
  },
  repo: {
    borderColor: colors.border,
    borderRadius: radius.md,
    borderStyle: 'solid',
    borderWidth: stroke.border,
    fontFamily: font.mono,
    fontSize: fontSize.sm,
    paddingBlock: space.s2,
    paddingInline: space.s3,
  },
});

Install

npx shadcn@latest add @madeui/collapsible

Usage

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '@/components/ui/collapsible';

Composition

<Collapsible>
  <CollapsibleTrigger />
  <CollapsibleContent />
</Collapsible>

File tree

Nested collapsibles build a file tree.

button.tsx
dialog.tsx
index.ts
package.json
import * as React from 'react';

import * as stylex from '@stylexjs/stylex';

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { space, fontSize, duration, stroke } from '@/lib/constants.stylex';
import { colors, font, radius } from '@/lib/tokens.stylex';

function ChevronIcon() {
  return (
    <svg
      width="14"
      height="14"
      viewBox={`0 0 16 16`}
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
      {...stylex.props(styles.chevron)}
    >
      <path d={`m6 3 5 5-5 5`} />
    </svg>
  );
}

function FolderIcon() {
  return (
    <svg
      width="14"
      height="14"
      viewBox={`0 0 16 16`}
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
    >
      <path d={`M2 4.5A1.5 1.5 0 0 1 3.5 3h2.6l1.2 1.5H12.5A1.5 1.5 0 0 1 14 6v6a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 12z`} />
    </svg>
  );
}

function FileIcon() {
  return (
    <svg
      width="14"
      height="14"
      viewBox={`0 0 16 16`}
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
    >
      <path d={`M4 2h5l3 3v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z`} />
      <path d={`M9 2v3h3`} />
    </svg>
  );
}

function Folder({
  name,
  defaultOpen,
  depth = 0,
  children,
}: {
  name: string;
  defaultOpen?: boolean;
  depth?: number;
  children: React.ReactNode;
}) {
  return (
    <Collapsible defaultOpen={defaultOpen}>
      <CollapsibleTrigger
        {...stylex.props(styles.row, indents.depth(depth))}
      >
        <ChevronIcon />
        <FolderIcon />
        <span>{name}</span>
      </CollapsibleTrigger>
      <CollapsibleContent>{children}</CollapsibleContent>
    </Collapsible>
  );
}

function File({ name, depth = 1 }: { name: string; depth?: number }) {
  return (
    <div {...stylex.props(styles.row, styles.file, indents.depth(depth))}>
      <span {...stylex.props(styles.fileSpacer)} />
      <FileIcon />
      <span>{name}</span>
    </div>
  );
}

export default function CollapsibleFileTree() {
  return (
    <div {...stylex.props(styles.tree)}>
      <Folder name="src" defaultOpen depth={0}>
        <Folder name="components" defaultOpen depth={1}>
          <File name="button.tsx" depth={2} />
          <File name="dialog.tsx" depth={2} />
        </Folder>
        <File name="index.ts" depth={1} />
      </Folder>
      <Folder name="public" depth={0}>
        <File name="favicon.ico" depth={1} />
      </Folder>
      <File name="package.json" depth={0} />
    </div>
  );
}

const styles = stylex.create({
  tree: {
    display: 'flex',
    flexDirection: 'column',
    fontFamily: font.mono,
    fontSize: fontSize.sm,
  },
  row: {
    alignItems: 'center',
    backgroundColor: {
      default: 'transparent',
      ':hover': colors.accent,
    },
    borderRadius: radius.sm,
    borderStyle: 'none',
    color: colors.foreground,
    cursor: 'pointer',
    display: 'flex',
    fontFamily: font.mono,
    fontSize: fontSize.sm,
    gap: space.s1,
    outline: {
      default: 'none',
      ':focus-visible': `${stroke.focus} solid ${colors.ring}`,
    },
    // Read by the chevron below — StyleX has no child selectors, so the
    // trigger's [data-panel-open] state travels via a custom property.
    '--file-tree-chevron-rotation': {
      default: null,
      '[data-panel-open]': '90deg',
    },
    paddingBlock: space.s1,
    paddingInline: space.s1,
    textAlign: 'left',
    width: '100%',
  },
  file: {
    color: colors.mutedForeground,
    cursor: 'default',
  },
  fileSpacer: {
    display: 'inline-block',
    width: space.s25,
  },
  chevron: {
    flexShrink: 0,
    transform: 'rotate(var(--file-tree-chevron-rotation, 0deg))',
    transitionDuration: duration.fast,
    transitionProperty: 'transform',
  },
});

const indents = stylex.create({
  depth: (depth: number) => ({
    paddingLeft: `calc(${space.s4} * ${depth})`,
  }),
});

API reference

Built on Base UI Collapsible. Collapsible and CollapsibleTrigger are Base UI parts re-exported unstyled — bring your own trigger via render (e.g. render={<Button />}). CollapsibleContent animates its height open and closed and accepts a style prop; style the inner content yourself. See the Base UI Collapsible API reference.

Was this page helpful?