Skip to main content

Collapsible Component Quickstart Guide

A simple, global collapsible wrapper with persistent state, cross-tab sync, and React hooks.


1. Setup Path Alias (Optional)

Add to tsconfig.json so you can import from @reusable/:

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@reusable/*": ["src/reusable/*"]
}
}
}

2. Install Files

Drop these under src/reusable/:

2.1 CollapsibleManager.ts

// src/reusable/CollapsibleManager.ts
// In-memory subscribers per storageKey
const subscribers: Record<string, ((v: boolean) => void)[]> = {}

// Subscribe to changes
export function subscribeCollapsible(key: string, cb: (v: boolean) => void) {
subscribers[key] ??= []
subscribers[key].push(cb)
}
export function unsubscribeCollapsible(key: string, cb: (v: boolean) => void) {
subscribers[key] = (subscribers[key] || []).filter((fn) => fn !== cb)
}

// Read persisted value (fallback to default)
export function getCollapsedGlobal(key: string, defaultState = false): boolean {
if (typeof window === "undefined") return defaultState
const raw = localStorage.getItem(key)
return raw !== null ? JSON.parse(raw) : defaultState
}

// Write + notify subscribers
export function setCollapsedGlobal(key: string, value: boolean) {
if (typeof window === "undefined") return
localStorage.setItem(key, JSON.stringify(value))
for (const fn of subscribers[key] || []) fn(value)
}

// Toggle helper
export function toggleCollapsedGlobal(key: string) {
const current = getCollapsedGlobal(key, false)
setCollapsedGlobal(key, !current)
}

// Cross-tab sync
if (typeof window !== "undefined") {
window.addEventListener("storage", (e) => {
if (e.key && subscribers[e.key]) {
const newVal = e.newValue === null ? false : JSON.parse(e.newValue)
for (const fn of subscribers[e.key]) fn(newVal)
}
})
}

2.2 Collapsible.tsx

// src/reusable/Collapsible.tsx
"use client"
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react"
import {
subscribeCollapsible,
unsubscribeCollapsible,
getCollapsedGlobal,
toggleCollapsedGlobal,
setCollapsedGlobal,
} from "./CollapsibleManager"

interface CollapsibleProps {
storageKey: string
children: ReactNode
collapsedClass?: string
expandedClass?: string
defaultCollapsed?: boolean
}

interface ContextType {
isCollapsed: boolean
toggle: () => void
setCollapsed: (v: boolean) => void
}

const CollapsibleContext = createContext<ContextType | null>(null)

export function useCollapsible() {
const ctx = useContext(CollapsibleContext)
if (!ctx) throw new Error("useCollapsible must be used inside <Collapsible>")
return ctx
}

export default function Collapsible({
storageKey,
children,
collapsedClass = "w-20",
expandedClass = "w-52",
defaultCollapsed = false,
}: CollapsibleProps) {
const [isCollapsed, setIsCollapsed] = useState(() =>
getCollapsedGlobal(storageKey, defaultCollapsed)
)

// Subscribe to global changes
useEffect(() => {
const cb = (val: boolean) => setIsCollapsed(val)
subscribeCollapsible(storageKey, cb)
return () => unsubscribeCollapsible(storageKey, cb)
}, [storageKey])

// Local controls mirror global store
const toggle = () => toggleCollapsedGlobal(storageKey)
const setCollapsed = (v: boolean) => setCollapsedGlobal(storageKey, v)

return (
<CollapsibleContext.Provider value={{ isCollapsed, toggle, setCollapsed }}>
<div
className={`transition-all duration-300 ${
isCollapsed ? collapsedClass : expandedClass
}`}
>
{children}
</div>
</CollapsibleContext.Provider>
)
}

3. Usage

3.1 Wrap a sidebar (or any panel)

import Collapsible from "@reusable/Collapsible"

;<Collapsible
storageKey="isSidebarCollapsed"
collapsedClass="w-16"
expandedClass="w-64"
>
<Sidebar userName={userName} />
</Collapsible>

3.2 Inside child components

import { useCollapsible } from "@reusable/Collapsible"
const { isCollapsed, toggle, setCollapsed } = useCollapsible()

3.3 Global control (anywhere)

import {
toggleCollapsedGlobal,
setCollapsedGlobal,
getCollapsedGlobal,
} from "@reusable/CollapsibleManager"

// Toggle
toggleCollapsedGlobal("isSidebarCollapsed")

// Collapse
setCollapsedGlobal("isSidebarCollapsed", true)

// Read state
const isCollapsed = getCollapsedGlobal("isSidebarCollapsed")

4. Auto-opening Submenus

To automatically open a parent menu when landing on a child route, choose one of two approaches:

Option A: Add parentKey to NavItem

  1. Extend your NavItem interface:

    export interface NavItem {
    key: string
    label: string
    icon: React.FC<SVGProps<SVGSVGElement>>
    path?: string
    badge?: boolean
    submenu?: NavItem[]
    onClick?: (router: NextRouter) => void
    /** ← new */
    parentKey?: string
    }
  2. Annotate each submenu item with its parent’s key:

    const mainNavItems: NavItem[] = [
    {
    key: "4",
    label: "Resumes",
    icon: ResumeIcon,
    submenu: [
    {
    key: "4-1",
    label: "Candidates",
    path: "/company/candidates",
    parentKey: "4",
    },
    // …
    ],
    },
    // …
    ]
  3. In your sidebar’s useEffect, auto‑open via parentKey:

    useEffect(() => {
    const match = items.find((i) => i.path && pathname.startsWith(i.path))
    if (match?.parentKey && !openKeys.includes(match.parentKey)) {
    setOpenKeys((keys) => [...keys, match.parentKey!])
    }
    }, [pathname, items, openKeys])

Option B: Infer parent dynamically

Without modifying NavItem, scan for the parent:

useEffect(() => {
const match = items.find((i) => i.path && pathname.startsWith(i.path))
const parent = items.find((i) =>
i.submenu?.some((sub) => sub.key === match?.key)
)
if (parent && !openKeys.includes(parent.key)) {
setOpenKeys((keys) => [...keys, parent.key])
}
}, [pathname, items, openKeys])

5. API Summary

Function / ComponentDescription
Collapsible (React component)Wraps content with collapse/expand animation
useCollapsible() (hook)Access isCollapsed, toggle(), setCollapsed()
subscribeCollapsible(key, cb)Listen for changes to key
unsubscribeCollapsible(key, cb)Remove listener
getCollapsedGlobal(key, default)Read persisted state
setCollapsedGlobal(key, value)Write state & notify subscribers
toggleCollapsedGlobal(key)Shortcut to toggle + notify