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.