Collapsible
An interactive component that expands/collapses a panel.
The Collapsible component is an interactive element that allows users to expand or collapse a panel of content. It features bold borders and shadows that match the Things design system. Built from scratch using React and native HTML elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/collapsible.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface CollapsibleContextValue {
open: boolean
onOpenChange: (open: boolean) => void
}
const CollapsibleContext = React.createContext<CollapsibleContextValue | undefined>(undefined)
const useCollapsible = () => {
const context = React.useContext(CollapsibleContext)
if (!context) {
throw new Error("Collapsible components must be used within Collapsible")
}
return context
}
interface CollapsibleProps {
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
disabled?: boolean
children: React.ReactNode
className?: string
}
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
({ open: controlledOpen, defaultOpen = false, onOpenChange, disabled = false, children, className, ...props }, ref) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const isControlled = controlledOpen !== undefined
const open = isControlled ? controlledOpen : uncontrolledOpen
const handleOpenChange = React.useCallback((newOpen: boolean) => {
if (disabled) return
if (!isControlled) {
setUncontrolledOpen(newOpen)
}
onOpenChange?.(newOpen)
}, [disabled, isControlled, onOpenChange])
return (
<CollapsibleContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
<div ref={ref} className={cn("w-full", className)} {...props}>
{children}
</div>
</CollapsibleContext.Provider>
)
}
)
Collapsible.displayName = "Collapsible"
interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
({ className, children, ...props }, ref) => {
const { open, onOpenChange } = useCollapsible()
return (
<button
ref={ref}
type="button"
onClick={() => onOpenChange(!open)}
className={cn(
"flex w-full items-center justify-between bg-primary text-primary-foreground p-4 font-bold transition-colors hover:bg-primary/90 border-2 border-foreground rounded-lg neobrutalism-shadow",
className
)}
{...props}
>
{children}
<svg
className={cn(
"h-4 w-4 shrink-0 transition-transform duration-200",
open && "rotate-180"
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
)
}
)
CollapsibleTrigger.displayName = "CollapsibleTrigger"
interface CollapsibleContentProps extends React.HTMLAttributes<HTMLDivElement> {}
const CollapsibleContent = React.forwardRef<HTMLDivElement, CollapsibleContentProps>(
({ className, children, ...props }, ref) => {
const { open } = useCollapsible()
return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all duration-200",
open ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
)}
{...props}
>
<div className={cn("p-4 bg-background border-2 border-foreground border-t-0 rounded-b-lg neobrutalism-shadow mt-2", className)}>
{children}
</div>
</div>
)
}
)
CollapsibleContent.displayName = "CollapsibleContent"
export { Collapsible, CollapsibleTrigger, CollapsibleContent }JavaScript: Copy this code into components/ui/collapsible.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const CollapsibleContext = React.createContext(undefined)
const useCollapsible = () => {
const context = React.useContext(CollapsibleContext)
if (!context) {
throw new Error("Collapsible components must be used within Collapsible")
}
return context
}
const Collapsible = React.forwardRef(
({ open: controlledOpen, defaultOpen = false, onOpenChange, disabled = false, children, className, ...props }, ref) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const isControlled = controlledOpen !== undefined
const open = isControlled ? controlledOpen : uncontrolledOpen
const handleOpenChange = React.useCallback((newOpen) => {
if (disabled) return
if (!isControlled) {
setUncontrolledOpen(newOpen)
}
onOpenChange?.(newOpen)
}, [disabled, isControlled, onOpenChange])
return (
<CollapsibleContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
<div ref={ref} className={cn("w-full", className)} {...props}>
{children}
</div>
</CollapsibleContext.Provider>
)
}
)
Collapsible.displayName = "Collapsible"
const CollapsibleTrigger = React.forwardRef(
({ className, children, ...props }, ref) => {
const { open, onOpenChange } = useCollapsible()
return (
<button
ref={ref}
type="button"
onClick={() => onOpenChange(!open)}
className={cn(
"flex w-full items-center justify-between bg-primary text-primary-foreground p-4 font-bold transition-colors hover:bg-primary/90 border-2 border-foreground rounded-lg neobrutalism-shadow",
className
)}
{...props}
>
{children}
<svg
className={cn(
"h-4 w-4 shrink-0 transition-transform duration-200",
open && "rotate-180"
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
)
}
)
CollapsibleTrigger.displayName = "CollapsibleTrigger"
const CollapsibleContent = React.forwardRef(
({ className, children, ...props }, ref) => {
const { open } = useCollapsible()
return (
<div
ref={ref}
className={cn(
"overflow-hidden transition-all duration-200",
open ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
)}
{...props}
>
<div className={cn("p-4 bg-background border-2 border-foreground border-t-0 rounded-b-lg neobrutalism-shadow mt-2", className)}>
{children}
</div>
</div>
)
}
)
CollapsibleContent.displayName = "CollapsibleContent"
export { Collapsible, CollapsibleTrigger, CollapsibleContent }Usage
TypeScript:
tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"
function MyComponent() {
return (
<Collapsible>
<CollapsibleTrigger>@marvellousz starred 3 repositories</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-2">
<div className="p-2 border-2 border-foreground rounded neobrutalism-shadow-sm">@radix-ui/primitives</div>
<div className="p-2 border-2 border-foreground rounded neobrutalism-shadow-sm">@radix-ui/colors</div>
<div className="p-2 border-2 border-foreground rounded neobrutalism-shadow-sm">@stitches/react</div>
</div>
</CollapsibleContent>
</Collapsible>
)
}JavaScript:
jsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"
function MyComponent() {
return (
<Collapsible>
<CollapsibleTrigger>@marvellousz starred 3 repositories</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-2">
<div className="p-2 border-2 border-foreground rounded neobrutalism-shadow-sm">@radix-ui/primitives</div>
<div className="p-2 border-2 border-foreground rounded neobrutalism-shadow-sm">@radix-ui/colors</div>
<div className="p-2 border-2 border-foreground rounded neobrutalism-shadow-sm">@stitches/react</div>
</div>
</CollapsibleContent>
</Collapsible>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.
Examples
@radix-ui/primitives
@radix-ui/colors
@stitches/react
This collapsible is open by default.