Sidebar
Overview
The Sidebar component is a versatile and animated navigation element that provides an elegant way to organize navigation links and actions in your application. It supports multiple positions, variants, and animations built with Framer Motion.
Preview
- Preview
- Code
Demo Window
Demo App
import React, { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
import { motion } from "framer-motion";
import {
Menu,
X,
} from "lucide-react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../../utils/cn";
interface LinkItem {
label: string;
href: string;
icon: React.ElementType;
}
interface SidebarProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof sidebarVariants> {
links: LinkItem[];
brandName?: string;
position?: "left" | "right" | "bottomLeft" | "bottomRight";
}
const sidebarVariants = cva("absolute h-full overflow-hidden transition-all", {
variants: {
position: {
left: "top-0 left-0",
right: "top-0 right-0",
bottomLeft: "bottom-0 left-0",
bottomRight: "bottom-0 right-0",
},
isOpen: {
true: "w-64 h-full",
false: `w-20 h-full`,
},
variant: {
default: "bg-background text-foreground",
dark: "bg-black text-white",
light: "bg-white text-gray-900 border-r",
glass: "bg-white/10 backdrop-blur-lg text-white",
},
direction: {
horizontal: "flex-row",
vertical: "flex-col items-start",
},
},
defaultVariants: {
position: "left",
isOpen: true,
variant: "default",
direction: "vertical",
},
});
interface SidebarContextType {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
toggle: () => void;
onClose: () => void;
onOpen: () => void;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
interface SidebarProviderProps {
children: ReactNode;
initialOpen?: boolean;
}
export const SidebarProvider: React.FC<SidebarProviderProps> = ({
children,
initialOpen = true
}) => {
const [isOpen, setIsOpen] = useState(initialOpen);
const toggle = useCallback(() => {
setIsOpen(prev => !prev);
}, []);
const onClose = useCallback(() => {
setIsOpen(false);
}, []);
const onOpen = useCallback(() => {
setIsOpen(true);
}, []);
const value: SidebarContextType = {
isOpen,
setIsOpen,
toggle,
onClose,
onOpen,
};
return (
<SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
);
};
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (context === undefined) {
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
};
const Sidebar: React.FC<SidebarProps> = ({
links,
brandName = "Brand",
position = "left",
variant,
className,
direction,
}) => {
const { isOpen, onClose, onOpen } = useSidebar();
const [isMobile, setIsMobile] = React.useState(false);
// breakpoint width
const bp = 768; // 1024;
React.useEffect(() => {
const check = () => {
const mobile = window.innerWidth < bp;
setIsMobile(mobile);
};
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, [bp]);
return (
<motion.div
initial={{ x: 0 }}
animate={{ x: 0 }}
transition={{ duration: 0.4 }}
className={cn(
sidebarVariants({ position, isOpen, variant, direction }),
isMobile ? !isOpen ? "w-0" : isOpen: '',
className
)}
>
{/* Sidebar Header */}
<div className="p-4 flex items-center justify-between gap-4">
{isOpen && <h1 className="text-xl font-bold">{brandName}</h1>}
{isOpen ? (
<button onClick={onClose}>
<span title="Close">
<X size={24} />
</span>
</button>
) : (
<button onClick={onOpen}>
<span title="Open">
<Menu size={24} />
</span>
</button>
)}
</div>
{/* Sidebar Links */}
<motion.nav
className={cn(
direction === "horizontal" ? "flex-row" : "flex-col",
"flex"
)}
>
{links.map((link, index) => (
<a
key={index}
href={link.href}
className="flex items-center p-4 gap-4 "
>
<link.icon size={24} />
{isOpen && <span>{link.label}</span>}
</a>
))}
</motion.nav>
</motion.div>
);
};
export default Sidebar;
Usage
Import the component:
import { Sidebar } from './components/ui';
Basic Usage
import { Home, Settings, User, Mail } from 'lucide-react';
function BasicSidebar() {
const links = [
{ label: 'Home', href: '/', icon: Home },
{ label: 'Profile', href: '/profile', icon: User },
{ label: 'Settings', href: '/settings', icon: Settings },
{ label: 'Contact', href: '/contact', icon: Mail },
];
return (
<Sidebar
links={links}
brandName="My App"
/>
);
}
Variants
- Preview
- Code
Demo Window
Demo App
<Sidebar
links={[
{ label: 'Home', href: '#', icon: Home },
{ label: 'Profile', href: '#', icon: User },
{ label: 'Settings', href: '#', icon: Settings },
{ label: 'Help', href: '#', icon: HelpCircle },
]}
brandName="Demo App"
variant="default"
position="left"
/>
Customization
With Custom Link Styling
function CustomStyledSidebar() {
const links = [
{ label: 'Home', href: '/', icon: Home },
{ label: 'Settings', href: '/settings', icon: Settings },
];
return (
<Sidebar
links={links}
brandName="Custom Links"
className="[&_a]:hover:bg-blue-500 [&_a]:transition-colors"
/>
);
}
Responsive Sidebar
Create a responsive sidebar that adapts to different screen sizes:
import { useState, useEffect } from 'react';
function ResponsiveSidebar() {
const [isOpen, setIsOpen] = useState(true);
useEffect(() => {
const handleResize = () => {
setIsOpen(window.innerWidth > 768);
};
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<Sidebar
links={links}
isOpen={isOpen}
onClose={() => setIsOpen(false)}
className="md:relative absolute"
/>
);
}