Glrk UI

Collapsible

A wrapper for Base UI Collapsible — a single trigger that expands and collapses a panel with smooth animation.

Basic
Right indicator (default)
Left indicator
Open by default

This panel starts expanded. User can collapse it.

Disabled
Controlled
External open state + built-in trigger
Panel
Keep mounted — DOM preserved when closed
Triggers
Custom element trigger (div)

Installation

npx shadcn@latest add @glrk-ui/collapsible

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

ui/collapsible.tsx
'use client'

import * as React from 'react'
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible'
import { ChevronDownIcon } from 'lucide-react'

import { cn } from '@/lib/utils'

function Collapsible(props: CollapsiblePrimitive.Root.Props) {
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}

function CollapsibleTrigger({
  className,
  children,
  indicatorAt = 'right',
  ...props
}: CollapsiblePrimitive.Trigger.Props & { indicatorAt?: indicatorAtT }) {
  const icon = (
    <ChevronDownIcon
      data-slot="collapsible-trigger-icon"
      className={cn(
        'pointer-events-none shrink-0 transition-transform duration-200 group-data-[panel-open]/collapsible-trigger:rotate-180',
        indicatorAt === 'right' && 'ml-auto',
      )}
    />
  )

  return (
    <CollapsiblePrimitive.Trigger
      data-slot="collapsible-trigger"
      className={cn(
        'group/collapsible-trigger flex w-full items-center gap-2 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer disabled:pointer-events-none disabled:opacity-50 **:data-[slot=collapsible-trigger-icon]:size-4 **:data-[slot=collapsible-trigger-icon]:text-muted-foreground',
        className,
      )}
      {...props}
    >
      {indicatorAt === 'left' && icon}
      {children}
      {indicatorAt === 'right' && icon}
    </CollapsiblePrimitive.Trigger>
  )
}

function CollapsibleContent({ className, ...props }: CollapsiblePrimitive.Panel.Props) {
  return (
    <CollapsiblePrimitive.Panel
      data-slot="collapsible-content"
      className={cn(
        'overflow-hidden text-sm h-(--collapsible-panel-height) transition-[height] ease-out data-ending-style:h-0 data-starting-style:h-0',
        className,
      )}
      {...props}
    />
  )
}

type CollapsibleWrapperProps = {
  trigger: React.ReactNode
  children: React.ReactNode
  triggerCls?: string
  contentCls?: string
  indicatorAt?: indicatorAtT
  triggerProps?: Omit<CollapsiblePrimitive.Trigger.Props, 'children' | 'className'>
  contentProps?: Omit<CollapsiblePrimitive.Panel.Props, 'children' | 'className'>
} & CollapsiblePrimitive.Root.Props

function CollapsibleWrapper({
  trigger,
  children,
  triggerCls,
  contentCls,
  indicatorAt = 'right',
  triggerProps,
  contentProps,
  ...props
}: CollapsibleWrapperProps) {
  return (
    <Collapsible {...props}>
      <CollapsibleTrigger className={cn(triggerCls)} indicatorAt={indicatorAt} {...triggerProps}>
        {trigger}
      </CollapsibleTrigger>
      <CollapsibleContent className={cn(contentCls)} {...contentProps}>
        {children}
      </CollapsibleContent>
    </Collapsible>
  )
}

export { Collapsible, CollapsibleTrigger, CollapsibleContent, CollapsibleWrapper }

Usage

Basic

import { CollapsibleWrapper } from "@/components/ui/collapsible"
import { ChevronDown } from "lucide-react"

<CollapsibleWrapper
  trigger={<><ChevronDown className="size-4" /> Details</>}
  className="w-72 rounded-lg border p-3"
>
  <p className="mt-2 text-sm text-muted-foreground">Panel content here.</p>
</CollapsibleWrapper>

Default open

<CollapsibleWrapper defaultOpen trigger="Details">
  Content visible on mount.
</CollapsibleWrapper>

Disabled

<CollapsibleWrapper disabled trigger="Details">
  Unreachable content.
</CollapsibleWrapper>

Controlled

const [open, setOpen] = useState(false)

<CollapsibleWrapper
  open={open}
  onOpenChange={setOpen}
  trigger="Details"
>
  Controlled from outside.
</CollapsibleWrapper>

Keep mounted

Panel DOM stays in tree when closed — preserves state, avoids remount:

<CollapsibleWrapper
  trigger="Details"
  contentProps={{ keepMounted: true }}
>
  State here persists when collapsed.
</CollapsibleWrapper>

Hidden until found

Closed panel content findable by browser Ctrl+F:

<CollapsibleWrapper
  trigger="Details"
  contentProps={{ hiddenUntilFound: true }}
>
  This text is findable even when collapsed.
</CollapsibleWrapper>

Custom element trigger

<CollapsibleWrapper
  trigger="Settings"
  triggerProps={{
    render: <div role="button" tabIndex={0} />,
    nativeButton: false,
  }}
  triggerCls="flex cursor-pointer items-center gap-2 rounded-md border px-3 py-1.5 text-sm"
>
  Content here.
</CollapsibleWrapper>

Using primitives

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

<Collapsible>
  <CollapsibleTrigger className="w-full justify-between">
    Title
    <ChevronDown className="size-4" />
  </CollapsibleTrigger>
  <CollapsibleContent keepMounted>
    Content
  </CollapsibleContent>
</Collapsible>

Reference

CollapsibleWrapper

Prop

Type