8.0k New

Collapsible

An interactive component which expands/collapses a panel.

Docs API Reference

Epicenter

Local-first, open source apps

Special Sponsor

Order #4189

Status Shipped
<script lang="ts">
  import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
  import * as Collapsible from "$lib/components/ui/collapsible/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
 
  let isOpen = $state(false);
</script>
 
<Collapsible.Root bind:open={isOpen} class="flex w-[350px] flex-col gap-2">
  <div class="flex items-center justify-between gap-4 px-4">
    <h4 class="text-sm font-semibold">Order #4189</h4>
    <Collapsible.Trigger>
      {#snippet child({ props })}
        <Button {...props} variant="ghost" size="icon" class="size-8">
          <ChevronsUpDownIcon />
          <span class="sr-only">Toggle details</span>
        </Button>
      {/snippet}
    </Collapsible.Trigger>
  </div>
  <div
    class="flex items-center justify-between rounded-md border px-4 py-2 text-sm"
  >
    <span class="text-muted-foreground">Status</span>
    <span class="font-medium">Shipped</span>
  </div>
  <Collapsible.Content class="flex flex-col gap-2">
    <div class="rounded-md border px-4 py-2 text-sm">
      <p class="font-medium">Shipping address</p>
      <p class="text-muted-foreground">100 Market St, San Francisco</p>
    </div>
    <div class="rounded-md border px-4 py-2 text-sm">
      <p class="font-medium">Items</p>
      <p class="text-muted-foreground">2x Studio Headphones</p>
    </div>
  </Collapsible.Content>
</Collapsible.Root>

Installation

pnpm dlx shadcn-svelte@latest add collapsible

Usage

<script lang="ts">
  import * as Collapsible from "$lib/components/ui/collapsible/index.js";
</script>
<Collapsible.Root>
  <Collapsible.Trigger>Can I use this in my project?</Collapsible.Trigger>
  <Collapsible.Content>
    Yes. Free to use for personal and commercial projects. No attribution
    required.
  </Collapsible.Content>
</Collapsible.Root>

Controlled State

Use bind:open to control the state.

<script lang="ts">
  import * as Collapsible from "$lib/components/ui/collapsible/index.js";
 
  let open = $state(false);
</script>
 
<Collapsible.Root bind:open>
  <Collapsible.Trigger>Toggle</Collapsible.Trigger>
  <Collapsible.Content>Content</Collapsible.Content>
</Collapsible.Root>

Basic

<script lang="ts">
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import * as Card from "$lib/components/ui/card/index.js";
  import * as Collapsible from "$lib/components/ui/collapsible/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
</script>
 
<Card.Root class="mx-auto w-full max-w-sm">
  <Card.Content>
    <Collapsible.Root class="rounded-md data-[state=open]:bg-muted">
      <Collapsible.Trigger>
        {#snippet child({ props })}
          <Button {...props} variant="ghost" class="group w-full">
            Product details
            <ChevronDownIcon
              class="ml-auto group-data-[state=open]:rotate-180"
            />
          </Button>
        {/snippet}
      </Collapsible.Trigger>
      <Collapsible.Content
        class="flex flex-col items-start gap-2 p-2.5 pt-0 text-sm"
      >
        <div>
          This panel can be expanded or collapsed to reveal additional content.
        </div>
        <Button size="xs">Learn More</Button>
      </Collapsible.Content>
    </Collapsible.Root>
  </Card.Content>
</Card.Root>

Settings Panel

Use a trigger button to reveal additional settings.

Radius

Set the corner radius of the element.

<script lang="ts">
  import MaximizeIcon from "@lucide/svelte/icons/maximize";
  import MinimizeIcon from "@lucide/svelte/icons/minimize";
  import * as Card from "$lib/components/ui/card/index.js";
  import * as Collapsible from "$lib/components/ui/collapsible/index.js";
  import * as Field from "$lib/components/ui/field/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import { Input } from "$lib/components/ui/input/index.js";
 
  let isOpen = $state(false);
</script>
 
<Card.Root class="mx-auto w-full max-w-xs" size="sm">
  <Card.Header>
    <Card.Title>Radius</Card.Title>
    <Card.Description>Set the corner radius of the element.</Card.Description>
  </Card.Header>
  <Card.Content>
    <Collapsible.Root bind:open={isOpen} class="flex items-start gap-2">
      <Field.Group class="grid w-full grid-cols-2 gap-2">
        <Field.Field>
          <Field.Label for="radius-top-left" class="sr-only"
            >Radius Top Left</Field.Label
          >
          <Input id="radius-top-left" placeholder="0" value={0} />
        </Field.Field>
        <Field.Field>
          <Field.Label for="radius-top-right" class="sr-only"
            >Radius Top Right</Field.Label
          >
          <Input id="radius-top-right" placeholder="0" value={0} />
        </Field.Field>
        <Collapsible.Content class="col-span-full grid grid-cols-subgrid gap-2">
          <Field.Field>
            <Field.Label for="radius-bottom-left" class="sr-only"
              >Radius Bottom Left</Field.Label
            >
            <Input id="radius-bottom-left" placeholder="0" value={0} />
          </Field.Field>
          <Field.Field>
            <Field.Label for="radius-bottom-right" class="sr-only"
              >Radius Bottom Right</Field.Label
            >
            <Input id="radius-bottom-right" placeholder="0" value={0} />
          </Field.Field>
        </Collapsible.Content>
      </Field.Group>
      <Collapsible.Trigger>
        {#snippet child({ props })}
          <Button {...props} variant="outline" size="icon">
            {#if isOpen}
              <MinimizeIcon />
            {:else}
              <MaximizeIcon />
            {/if}
          </Button>
        {/snippet}
      </Collapsible.Trigger>
    </Collapsible.Root>
  </Card.Content>
</Card.Root>

File Tree

Use nested collapsibles to build a file tree.

<script lang="ts">
  import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
  import FileIcon from "@lucide/svelte/icons/file";
  import FolderIcon from "@lucide/svelte/icons/folder";
  import * as Card from "$lib/components/ui/card/index.js";
  import * as Collapsible from "$lib/components/ui/collapsible/index.js";
  import * as Tabs from "$lib/components/ui/tabs/index.js";
  import { Button } from "$lib/components/ui/button/index.js";
 
  type FileTreeItem =
    | { name: string }
    | { name: string; items: FileTreeItem[] };
 
  const fileTree: FileTreeItem[] = [
    {
      name: "components",
      items: [
        {
          name: "ui",
          items: [
            { name: "button.svelte" },
            { name: "card.svelte" },
            { name: "dialog.svelte" },
            { name: "input.svelte" },
            { name: "select.svelte" },
            { name: "table.svelte" }
          ]
        },
        { name: "login-form.svelte" },
        { name: "register-form.svelte" }
      ]
    },
    {
      name: "lib",
      items: [{ name: "utils.ts" }, { name: "cn.ts" }, { name: "api.ts" }]
    },
    {
      name: "hooks",
      items: [
        { name: "use-media-query.svelte.ts" },
        { name: "use-debounce.svelte.ts" },
        { name: "use-local-storage.svelte.ts" }
      ]
    },
    {
      name: "types",
      items: [{ name: "index.d.ts" }, { name: "api.d.ts" }]
    },
    {
      name: "public",
      items: [{ name: "favicon.ico" }, { name: "logo.svg" }, { name: "images" }]
    },
    { name: "app.html" },
    { name: "+layout.svelte" },
    { name: "app.css" },
    { name: "package.json" },
    { name: "tsconfig.json" },
    { name: "README.md" },
    { name: ".gitignore" }
  ];
</script>
 
{#snippet treeItem(fileItem: FileTreeItem)}
  {#if "items" in fileItem}
    <Collapsible.Root>
      <Collapsible.Trigger>
        {#snippet child({ props })}
          <Button
            {...props}
            variant="ghost"
            size="sm"
            class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground"
          >
            <ChevronRightIcon
              class="transition-transform group-data-[state=open]:rotate-90"
            />
            <FolderIcon />
            {fileItem.name}
          </Button>
        {/snippet}
      </Collapsible.Trigger>
      <Collapsible.Content class="mt-1 ml-5 style-lyra:ml-4">
        <div class="flex flex-col gap-1">
          {#each fileItem.items as child (child.name)}
            {@render treeItem(child)}
          {/each}
        </div>
      </Collapsible.Content>
    </Collapsible.Root>
  {:else}
    <Button
      variant="link"
      size="sm"
      class="w-full justify-start gap-2 text-foreground"
    >
      <FileIcon />
      <span>{fileItem.name}</span>
    </Button>
  {/if}
{/snippet}
 
<Card.Root class="mx-auto w-full max-w-[16rem] gap-2" size="sm">
  <Card.Header>
    <Tabs.Root value="explorer">
      <Tabs.List class="w-full">
        <Tabs.Trigger value="explorer">Explorer</Tabs.Trigger>
        <Tabs.Trigger value="settings">Outline</Tabs.Trigger>
      </Tabs.List>
    </Tabs.Root>
  </Card.Header>
  <Card.Content>
    <div class="flex flex-col gap-1">
      {#each fileTree as item (item.name)}
        {@render treeItem(item)}
      {/each}
    </div>
  </Card.Content>
</Card.Root>