Introducing Aceternity UI Pro - Premium component packs and templates to build awesome websites.
Logo

Sidebar

Expandable sidebar that expands on hover, mobile responsive and dark mode support

Installation

Install util dependencies

npm i framer-motion clsx tailwind-merge @tabler/icons-react

Add util file

lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Copy the source code

components/ui/sidebar.tsx

"use client";
import { cn } from "@/lib/utils";
import Link, { LinkProps } from "next/link";
import React, { useState, createContext, useContext } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { IconMenu2, IconX } from "@tabler/icons-react";
 
interface Links {
  label: string;
  href: string;
  icon: React.JSX.Element | React.ReactNode;
}
 
interface SidebarContextProps {
  open: boolean;
  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
  animate: boolean;
}
 
const SidebarContext = createContext<SidebarContextProps | undefined>(
  undefined
);
 
export const useSidebar = () => {
  const context = useContext(SidebarContext);
  if (!context) {
    throw new Error("useSidebar must be used within a SidebarProvider");
  }
  return context;
};
 
export const SidebarProvider = ({
  children,
  open: openProp,
  setOpen: setOpenProp,
  animate = true,
}: {
  children: React.ReactNode;
  open?: boolean;
  setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
  animate?: boolean;
}) => {
  const [openState, setOpenState] = useState(false);
 
  const open = openProp !== undefined ? openProp : openState;
  const setOpen = setOpenProp !== undefined ? setOpenProp : setOpenState;
 
  return (
    <SidebarContext.Provider value={{ open, setOpen, animate: animate }}>
      {children}
    </SidebarContext.Provider>
  );
};
 
export const Sidebar = ({
  children,
  open,
  setOpen,
  animate,
}: {
  children: React.ReactNode;
  open?: boolean;
  setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
  animate?: boolean;
}) => {
  return (
    <SidebarProvider open={open} setOpen={setOpen} animate={animate}>
      {children}
    </SidebarProvider>
  );
};
 
export const SidebarBody = (props: React.ComponentProps<typeof motion.div>) => {
  return (
    <>
      <DesktopSidebar {...props} />
      <MobileSidebar {...(props as React.ComponentProps<"div">)} />
    </>
  );
};
 
export const DesktopSidebar = ({
  className,
  children,
  ...props
}: React.ComponentProps<typeof motion.div>) => {
  const { open, setOpen, animate } = useSidebar();
  return (
    <>
      <motion.div
        className={cn(
          "h-full px-4 py-4 hidden  md:flex md:flex-col bg-neutral-100 dark:bg-neutral-800 w-[300px] flex-shrink-0",
          className
        )}
        animate={{
          width: animate ? (open ? "300px" : "60px") : "300px",
        }}
        onMouseEnter={() => setOpen(true)}
        onMouseLeave={() => setOpen(false)}
        {...props}
      >
        {children}
      </motion.div>
    </>
  );
};
 
export const MobileSidebar = ({
  className,
  children,
  ...props
}: React.ComponentProps<"div">) => {
  const { open, setOpen } = useSidebar();
  return (
    <>
      <div
        className={cn(
          "h-10 px-4 py-4 flex flex-row md:hidden  items-center justify-between bg-neutral-100 dark:bg-neutral-800 w-full"
        )}
        {...props}
      >
        <div className="flex justify-end z-20 w-full">
          <IconMenu2
            className="text-neutral-800 dark:text-neutral-200"
            onClick={() => setOpen(!open)}
          />
        </div>
        <AnimatePresence>
          {open && (
            <motion.div
              initial={{ x: "-100%", opacity: 0 }}
              animate={{ x: 0, opacity: 1 }}
              exit={{ x: "-100%", opacity: 0 }}
              transition={{
                duration: 0.3,
                ease: "easeInOut",
              }}
              className={cn(
                "fixed h-full w-full inset-0 bg-white dark:bg-neutral-900 p-10 z-[100] flex flex-col justify-between",
                className
              )}
            >
              <div
                className="absolute right-10 top-10 z-50 text-neutral-800 dark:text-neutral-200"
                onClick={() => setOpen(!open)}
              >
                <IconX />
              </div>
              {children}
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </>
  );
};
 
export const SidebarLink = ({
  link,
  className,
  ...props
}: {
  link: Links;
  className?: string;
  props?: LinkProps;
}) => {
  const { open, animate } = useSidebar();
  return (
    <Link
      href={link.href}
      className={cn(
        "flex items-center justify-start gap-2  group/sidebar py-2",
        className
      )}
      {...props}
    >
      {link.icon}
 
      <motion.span
        animate={{
          display: animate ? (open ? "inline-block" : "none") : "inline-block",
          opacity: animate ? (open ? 1 : 0) : 1,
        }}
        className="text-neutral-700 dark:text-neutral-200 text-sm group-hover/sidebar:translate-x-1 transition duration-150 whitespace-pre inline-block !p-0 !m-0"
      >
        {link.label}
      </motion.span>
    </Link>
  );
};

Example

Default sidebar open

use the prop animate={false} to disable the animation

Props

SidebarProvider Props

Prop NameTypeDefaultDescription
childrenReact.ReactNode-The content to be rendered inside the provider.
openbooleanfalseControls the open state of the sidebar.
setOpenReact.Dispatch<React.SetStateAction<boolean>>-Function to set the open state of the sidebar.
Prop NameTypeDefaultDescription
childrenReact.ReactNode-The content to be rendered inside the sidebar.
openbooleanfalseControls the open state of the sidebar.
setOpenReact.Dispatch<React.SetStateAction<boolean>>-Function to set the open state of the sidebar.
animatebooleantrueControls the animation of the sidebar. Put false if you want to disable animation

SidebarBody Props

Prop NameTypeDefaultDescription
propsReact.ComponentProps<typeof motion.div>-Props to be passed to the motion.div component.

DesktopSidebar Props

Prop NameTypeDefaultDescription
classNamestring-Additional class names for styling.
childrenReact.ReactNode-The content to be rendered inside the desktop sidebar.
propsReact.ComponentProps<typeof motion.div>-Props to be passed to the motion.div component.

MobileSidebar Props

Prop NameTypeDefaultDescription
classNamestring-Additional class names for styling.
childrenReact.ReactNode-The content to be rendered inside the mobile sidebar.
propsReact.ComponentProps<"div">-Props to be passed to the div component.
Prop NameTypeDefaultDescription
linkLinks-The link object containing label, href, and icon.
classNamestring-Additional class names for styling.
propsLinkProps-Props to be passed to the Link component.
PropertyTypeDescription
labelstringThe text label for the link.
hrefstringThe URL the link points to.
iconReact.JSX.Element | React.ReactNodeThe icon to be displayed alongside the link.

SidebarContextProps Interface

PropertyTypeDescription
openbooleanIndicates whether the sidebar is open.
setOpenReact.Dispatch<React.SetStateAction<boolean>>Function to set the open state of the sidebar.

Get beautiful, hand-crafted templates and components with Aceternity UI Pro

Professional, beautiful and elegant templates for your business. Get the best component packs and templates with Aceternity UI Pro.

Check website

Manu is an artist, I didn't know what I wanted when we started, but his intuition and eye for design more than made up for it. We went from “I want something dark theme and high...

John Ferry

President at TAC, CEO at Rogue

A product by Aceternity
Building in public at @mannupaaji