All components
Side Panel
modalsSliding side panel component with customizable positioning and animations
responsive · 560px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/side-panel.jsonUsage
"use client"
import { useState } from "react"
import { SidePanel } from "@/registry/cult-ui/side-panel"
function MenuIcon({ open }: { open: boolean }) {
return (
<div className="flex flex-col gap-1 w-5 h-5 justify-center">
<span
className={`block h-0.5 bg-white transition-all duration-300 ${open ? "rotate-45 translate-y-1.5" : ""}`}
/>
<span
className={`block h-0.5 bg-white transition-all duration-300 ${open ? "opacity-0" : ""}`}
/>
<span
className={`block h-0.5 bg-white transition-all duration-300 ${open ? "-rotate-45 -translate-y-1.5" : ""}`}
/>
</div>
)
}
const navItems = [
{ label: "Dashboard", description: "Overview & metrics" },
{ label: "Projects", description: "Active work" },
{ label: "Analytics", description: "Charts & reports" },
{ label: "Settings", description: "Account & preferences" },
]
export default function Demo() {
const [open, setOpen] = useState(false)
return (
<div className="flex items-start justify-start w-full min-h-[480px] bg-background">
<SidePanel
panelOpen={open}
handlePanelOpen={() => setOpen((v) => !v)}
renderButton={(toggle) => (
<button
onClick={toggle}
className="flex items-center gap-2 text-white text-sm font-medium focus:outline-none"
aria-label={open ? "Close menu" : "Open menu"}
>
<MenuIcon open={open} />
{open && <span className="whitespace-nowrap">Close</span>}
</button>
)}
>
<nav className="px-4 pb-6 flex flex-col gap-1">
{navItems.map((item) => (
<div
key={item.label}
className="rounded-lg px-3 py-2 cursor-pointer hover:bg-neutral-700 transition-colors"
>
<p className="text-white text-sm font-medium">{item.label}</p>
<p className="text-neutral-400 text-xs">{item.description}</p>
</div>
))}
</nav>
</SidePanel>
</div>
)
}Component source
"use client"
import React, { forwardRef, ReactNode } from "react"
import { AnimatePresence, motion, MotionConfig } from "motion/react"
import useMeasure from "react-use-measure"
import { cn } from "@/lib/utils"
type PanelContainerProps = {
panelOpen: boolean
handlePanelOpen: () => void
className?: string
videoUrl?: string
renderButton?: (handleToggle: () => void) => ReactNode
children: ReactNode
}
const sectionVariants = {
open: {
width: "97%",
transition: {
duration: 0.3,
ease: [0.42, 0, 0.58, 1] as const,
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
closed: {
transition: { duration: 0.2, ease: [0.42, 0, 0.58, 1] as const },
},
}
const sharedTransition = { duration: 0.6, ease: [0.42, 0, 0.58, 1] as const }
export const SidePanel = forwardRef<HTMLDivElement, PanelContainerProps>(
({ panelOpen, handlePanelOpen, className, renderButton, children }, ref) => {
const [measureRef, bounds] = useMeasure()
return (
<ResizablePanel>
<motion.div
className={cn(
"bg-neutral-900 rounded-r-[44px] w-[160px] md:w-[260px]",
className
)}
animate={panelOpen ? "open" : "closed"}
variants={sectionVariants}
transition={{ duration: 0.2, ease: [0.42, 0, 0.58, 1] as const }}
>
<motion.div
animate={{ height: bounds.height > 0 ? bounds.height : 0.1 }}
className="h-auto"
transition={{ type: "spring", bounce: 0.02, duration: 0.65 }}
>
<div ref={measureRef}>
<AnimatePresence mode="popLayout">
<motion.div
exit={{ opacity: 0 }}
transition={{
...sharedTransition,
duration: sharedTransition.duration / 2,
}}
key="form"
>
<div
className={cn(
"flex items-center w-full justify-start pl-4 md:pl-4 py-1 md:py-3",
panelOpen ? "pr-3" : ""
)}
>
{renderButton && renderButton(handlePanelOpen)}
</div>
{panelOpen && (
<motion.div
exit={{ opacity: 0 }}
transition={sharedTransition}
>
{children}
</motion.div>
)}
</motion.div>
</AnimatePresence>
</div>
</motion.div>
</motion.div>
</ResizablePanel>
)
}
)
SidePanel.displayName = "SidePanel"
export default SidePanel
type ResizablePanelProps = {
children: React.ReactNode
}
const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
({ children }, ref) => {
const transition = {
type: "tween" as const,
ease: [0.42, 0, 0.58, 1] as const,
duration: 0.4,
}
return (
<MotionConfig transition={transition}>
<div className="flex w-full flex-col items-start">
<div className="mx-auto w-full">
<div
ref={ref}
className={cn(
children ? "rounded-r-none" : "rounded-sm",
"relative overflow-hidden"
)}
>
{children}
</div>
</div>
</div>
</MotionConfig>
)
}
)
ResizablePanel.displayName = "ResizablePanel"Source: Cult UI