Collapsible

free

Collapsible keeps optional content reachable without expanding the page by default.

motion
Preview
Dark
import Collapsible from '@/components/ui/Collapsible';

export default function UsageDemo() {
  return (
    <div className="w-full max-w-md">
      <Collapsible className="rounded-lg border bg-card">
        <Collapsible.Trigger>Shipping details</Collapsible.Trigger>
        <Collapsible.Content className="px-4 pb-4">
          <p className="text-muted-foreground">
            Orders are processed within one business day and ship with tracked
            delivery. You will receive a confirmation email once the parcel
            leaves the warehouse.
          </p>
        </Collapsible.Content>
      </Collapsible>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Collapsible

Examples

Basic

Collapsible starts closed. Set defaultOpen to render it expanded on first paint while keeping it uncontrolled.

Preview
Dark

This region is expanded on first render and can still be collapsed.

import Collapsible from '@/components/ui/Collapsible';

export default function BasicDemo() {
  return (
    <div className="grid w-full gap-4">
      <Collapsible className="rounded-lg border bg-card">
        <Collapsible.Trigger>Closed by default</Collapsible.Trigger>
        <Collapsible.Content className="px-4 pb-4">
          <p className="text-muted-foreground">
            This region is hidden until the trigger is activated.
          </p>
        </Collapsible.Content>
      </Collapsible>

      <Collapsible
        defaultOpen
        className="rounded-lg border bg-card"
      >
        <Collapsible.Trigger>Open by default</Collapsible.Trigger>
        <Collapsible.Content className="px-4 pb-4">
          <p className="text-muted-foreground">
            This region is expanded on first render and can still be collapsed.
          </p>
        </Collapsible.Content>
      </Collapsible>
    </div>
  );
}

Controlled

Pass open and onOpenChange to drive collapsible state from outside. Here, two external buttons step through a list of sections while a label reflects which one is currently expanded.

Preview
Dark

Expanded: setup-guide

Install the CLI, scaffold a starter, and connect your first component to a live project.

import { useState } from 'react';
import Button from '@/components/ui/Button';
import Collapsible from '@/components/ui/Collapsible';
import { ChevronUp, ChevronDown } from '@/components/ui/Icons';

const sections = [
  {
    key: 'setup-guide',
    title: 'Setup guide',
    description:
      'Install the CLI, scaffold a starter, and connect your first component to a live project.',
  },
  {
    key: 'api-reference',
    title: 'API reference',
    description:
      'Every prop, type, and default for each component, generated from the source you install.',
  },
  {
    key: 'troubleshooting',
    title: 'Troubleshooting',
    description:
      'Fixes for the errors that come up most often during install, build, and updates.',
  },
];

export default function ControlledDemo() {
  const [activeIndex, setActiveIndex] = useState(0);
  const active = sections[activeIndex];

  return (
    <div className="w-full max-w-md">
      <div className="mb-2 flex items-center justify-between">
        <p className="text-muted-foreground ps-4">
          Expanded: <span className="font-medium text-foreground">{active.key}</span>
        </p>
        <div className="flex items-center gap-2">
          <Button
            size="sm"
            variant="subtle"
            disabled={activeIndex === 0}
            onClick={() => setActiveIndex((index) => index - 1)}
            icon={<ChevronUp />}
            aria-label="Expand previous section"
          />
          <Button
            size="sm"
            variant="subtle"
            disabled={activeIndex === sections.length - 1}
            onClick={() => setActiveIndex((index) => index + 1)}
            icon={<ChevronDown />}
            aria-label="Expand next section"
          />
        </div>
      </div>

      <div className="divide-y divide-border">
        {sections.map((section, index) => (
          <Collapsible
            key={section.key}
            open={index === activeIndex}
            onOpenChange={(next) => next && setActiveIndex(index)}
          >
            <Collapsible.Trigger>{section.title}</Collapsible.Trigger>
            <Collapsible.Content className="px-4 pb-4">
              <p className="text-muted-foreground">
                {section.description}
              </p>
            </Collapsible.Content>
          </Collapsible>
        ))}
      </div>
    </div>
  );
}

Accordion

Drive several collapsibles from one piece of state so only a single panel is open at a time — the common FAQ pattern.

Preview
Dark

Run the add command from the Installation section. The CLI copies the source into your project and installs any required dependencies.

import { useState } from 'react';
import Collapsible from '@/components/ui/Collapsible';

const faqs = [
  {
    question: 'How do I install a component?',
    answer:
      'Run the add command from the Installation section. The CLI copies the source into your project and installs any required dependencies.',
  },
  {
    question: 'Can I use it without a provider?',
    answer:
      'Yes. Every component falls back to sensible defaults, so it renders standalone. Mount a provider only when you want to theme across the whole app.',
  },
  {
    question: 'Is the source mine to edit?',
    answer:
      'The files land in your codebase, so you can change the markup, styles, and behavior to fit your product.',
  },
];

export default function AccordionDemo() {
  const [openIndex, setOpenIndex] = useState<number | null>(0);

  return (
    <div className="grid w-full max-w-md gap-2">
      {faqs.map((faq, index) => (
        <Collapsible
          key={faq.question}
          open={openIndex === index}
          onOpenChange={(next) => setOpenIndex(next ? index : null)}
          className="rounded-lg border bg-card"
        >
          <Collapsible.Trigger>{faq.question}</Collapsible.Trigger>
          <Collapsible.Content className="px-4 pb-4">
            <p className="text-muted-foreground">{faq.answer}</p>
          </Collapsible.Content>
        </Collapsible>
      ))}
    </div>
  );
}

Custom trigger

Pass a function to Collapsible.Trigger to render a fully custom trigger. It receives isOpen and toggle, so you can build your own control instead of the default button.

Preview
Dark
import Collapsible from '@/components/ui/Collapsible';
import Button from '@/components/ui/Button';
import { Plus, Minus } from '@/components/ui/Icons';

export default function CustomTriggerDemo() {
  return (
    <div className="w-full max-w-md">
      <Collapsible>
        <Collapsible.Trigger>
          {({ isOpen, toggle }) => (
            <button
              className="py-2 px-4 rounded-lg bg-secondary hover:bg-secondary-active w-full text-start"
              onClick={toggle}
            >
              <span className="inline-flex items-center justify-between gap-2">
                {
                  isOpen ? (
                    <Minus />
                  ) : (
                    <Plus />
                  )
                }
                <span>Advanced settings</span>
              </span>
            </button>
          )}
        </Collapsible.Trigger>
        <Collapsible.Content className="py-4">
          <p className="text-muted-foreground">
            The render-prop trigger gives you full control over the markup while
            Collapsible keeps managing the open state and the content animation.
          </p>
        </Collapsible.Content>
      </Collapsible>
    </div>
  );
}

API

Collapsible is a compound component: a root Collapsible that owns the open state, Collapsible.Trigger for the toggle, and Collapsible.Content for the animated region.

Collapsible

Collapsible accepts the props below plus native props for the root div.

PropDescriptionTypeDefault
childrenTrigger and content parts rendered inside the collapsible.ReactNode-
classNameClass names applied to the root div.string-
defaultOpenInitial open state for uncontrolled usage.booleanfalse
onOpenChangeCalled with the next open state whenever it changes.(open: boolean) => void-
openControls the open state. Provide with onOpenChange for controlled usage.boolean-
...nativeDivPropsNative props for the root div.ComponentProps<'div'>-

Collapsible.Trigger

Renders the default toggle button, or a custom trigger when children is a function.

PropDescriptionTypeDefault
childrenTrigger label, or a render function that receives isOpen and toggle for a custom trigger.ReactNode | CollapsibleTriggerRender-
classNameClass names applied to the default trigger button. Ignored when children is a function.string-

Collapsible.Content

Wraps the region that expands and collapses. Accepts the props below plus native props for the div.

PropDescriptionTypeDefault
childrenContent revealed when the collapsible is open.ReactNode-
classNameClass names applied to the animated content wrapper.string-
defaultOverflowHiddenApplies overflow-hidden so content is clipped during the height animation.booleantrue
...nativeDivPropsNative props for the content div.ComponentProps<'div'>-

Types

type CollapsibleTriggerRender = (props: {
  isOpen: boolean;
  toggle: () => void;
}) => ReactNode;