implement
Collapsible

Collapsible

One section of content that opens and closes.

@ieedan starred 3 packages

@implementjs/core
import { Div, H4, Span } from "@implementjs/core";
import { ChevronsUpDownIcon } from "@implementjs/lucide";
import {
	Collapsible,
	CollapsibleContent,
	CollapsibleTrigger,
} from "@/lib/components/ui/collapsible";

function Repo(name: string) {
	return Div({ class: "rounded-md border px-4 py-2 font-mono text-sm" }, name);
}

export default function CollapsibleDemo() {
	return Collapsible(
		{ class: "w-full max-w-md" },
		Div(
			{ class: "flex items-center justify-between gap-2 px-4" },
			H4({ class: "text-sm font-semibold" }, "@ieedan starred 3 packages"),
			CollapsibleTrigger(
				{ size: "icon-sm" },
				ChevronsUpDownIcon({ "aria-hidden": true, class: "size-4" }),
				Span({ class: "sr-only" }, "Toggle"),
			),
		),
		Repo("@implementjs/core"),
		CollapsibleContent(
			{ class: "flex flex-col gap-2" },
			Repo("@implementjs/primitives"),
			Repo("@implementjs/lucide"),
		),
	);
}

Installation

npx jsrepo add @implementjs/ui/collapsible

jsrepo pulls button along with it.

Usage

An accordion without the group: one trigger, one panel, no shared state. CollapsibleTrigger renders through the button styles and defaults to ghost, so it reads as a control without a filled background — pass variant and size to change that.

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

Collapsible(
	CollapsibleTrigger({ size: "icon-sm" }, ChevronsUpDownIcon({ "aria-hidden": true })),
	CollapsibleContent("@implementjs/kit", "@implementjs/primitives"),
);

Animation

The slide comes from the collapsible-down / collapsible-up keyframes in your stylesheet — the introduction has the block. Without them the panel shows and hides without animating.

CollapsibleContent puts your class on an inner Div, so padding you add does not fight the height animation.

API Reference

Every prop the styling does not consume is forwarded to the Collapsible primitive, so the tables below are the whole surface — the behavior props and the styling ones together.

Collapsible

The root. Owns whether the content is open and provides that to the parts inside it. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
openSignal<boolean> | booleanfalseThe open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state.
Data attributeValue
[data-collapsible-root]Present
[data-state]"open" | "closed"

CollapsibleTrigger

Toggles the content open and closed. Renders a Button; extra props are forwarded onto it.

PropTypeDefaultDescription
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link""ghost"Which button style the part renders with. Also set as data-variant.
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""default"The button's height and padding scale. Also set as data-size.
Data attributeValue
[data-collapsible-trigger]Present
[data-state]"open" | "closed"

CollapsibleContent

The body. Hidden with the `hidden` attribute while closed; a close animation on it finishes before the attribute is set. class lands on an inner Div, so the animated element keeps the overflow and height classes it needs. The slide comes from the collapsible keyframes in your stylesheet. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
hiddenUntilFoundbooleanfalseClosed content uses hidden="until-found" so find-in-page can reveal it.
Data attributeValue
[data-collapsible-content]Present
[data-state]"open" | "closed"
CSS variableDescription
--ip-collapsible-content-heightThe natural height of the content, for open/close animations.
--ip-collapsible-content-widthThe natural width of the content, for open/close animations.