Layout
Collapsible.
Shows and hides a section of content with an animated height.
miner-02 is watching 4 servers
computer-42Online
"use client";
import * as React from "react";
import { ChevronsUpDownIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
function Row({ name, status }: { name: string; status: string }) {
return (
<div className="flex items-center justify-between rounded-md border bg-card px-4 py-2.5">
<span className="font-mono text-sm">{name}</span>
<span className="font-mono text-[10px] tracking-[0.2em] text-success uppercase">{status}</span>
</div>
);
}
export default function CollapsibleDemo() {
const [open, setOpen] = React.useState(false);
return (
<Collapsible open={open} onOpenChange={setOpen} className="flex w-full max-w-[340px] flex-col gap-2">
<div className="flex items-center justify-between gap-4 px-1">
<h4 className="font-display text-sm font-medium">miner-02 is watching 4 servers</h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="icon-sm" aria-label={open ? "Show fewer servers" : "Show 3 more servers"}>
<ChevronsUpDownIcon />
</Button>
</CollapsibleTrigger>
</div>
<Row name="computer-42" status="Online" />
<CollapsibleContent className="flex flex-col gap-2">
<Row name="computer-17" status="Online" />
<Row name="computer-33" status="Idle" />
<Row name="computer-58" status="Online" />
</CollapsibleContent>
{!open && (
<CollapsibleTrigger className="cursor-pointer px-1 text-left font-mono text-[10px] tracking-[0.2em] text-muted-foreground uppercase outline-none hover:text-primary focus-visible:text-primary">
+ 3 more servers
</CollapsibleTrigger>
)}
</Collapsible>
);
}Installation#
With the CLI
$npx lanterncn add collapsibleThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/collapsible. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-uiCopy the source into your project
components/ui/collapsible.tsx"use client"; import * as React from "react"; import { Collapsible as CollapsiblePrimitive } from "radix-ui"; import { cn } from "@/lib/utils"; function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; } function CollapsibleTrigger({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />; } function CollapsibleContent({ className, ...props }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { return ( <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" className={cn( "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className, )} {...props} /> ); } export { Collapsible, CollapsibleTrigger, CollapsibleContent };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";<Collapsible>
<CollapsibleTrigger>Show servers</CollapsibleTrigger>
<CollapsibleContent>computer-17, computer-33</CollapsibleContent>
</Collapsible>Examples#
File tree
Computer 42 / disk
- hub.lua
- guestbook.lua
- config.json
- README.txt
import { ChevronRightIcon, FileIcon, FolderIcon } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
type Node = string | [string, Node[]];
const tree: Node[] = [
[
"startup",
["boot.lua", ["drivers", ["modem.lua", "monitor.lua"]]],
],
["programs", ["hub.lua", "guestbook.lua", ["turtle", ["mine.lua", "refuel.lua"]]]],
"config.json",
"README.txt",
];
function TreeNode({ node, defaultOpen }: { node: Node; defaultOpen?: boolean }) {
if (typeof node === "string") {
return (
<li>
<span className="flex items-center gap-2 rounded-sm px-2 py-1 text-muted-foreground">
<FileIcon className="size-3.5 shrink-0" />
{node}
</span>
</li>
);
}
const [name, children] = node;
return (
<li>
<Collapsible defaultOpen={defaultOpen} className="group/folder">
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-left text-foreground outline-none hover:bg-secondary focus-visible:ring-2 focus-visible:ring-ring/25">
<ChevronRightIcon className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/folder:rotate-90" />
<FolderIcon className="size-3.5 shrink-0 text-primary" />
{name}
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="ml-[15px] border-l pl-2">
{children.map((child) => (
<TreeNode key={typeof child === "string" ? child : child[0]} node={child} />
))}
</ul>
</CollapsibleContent>
</Collapsible>
</li>
);
}
export default function CollapsibleFileTree() {
return (
<div className="w-full max-w-[280px] rounded-lg border bg-card p-3">
<div className="mb-2 px-2 font-mono text-[10px] font-semibold tracking-[0.2em] text-success uppercase">
Computer 42 / disk
</div>
<ul className="font-mono text-[13px]">
{tree.map((node) => (
<TreeNode key={typeof node === "string" ? node : node[0]} node={node} defaultOpen={node[0] === "programs"} />
))}
</ul>
</div>
);
}