ReactHustle

How to Create a Responsive Collapsible Sidebar in React/NextJS using TailwindCSS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create a Responsive Collapsible Sidebar in React/NextJS using TailwindCSS

Hello, hustlers! Implementing layout can sometimes be tricky, especially when you're implementing multiple behaviors for a sidebar component. I was studying how to implement sidebars from scratch using tailwind and I thought it would be a good idea to share it here.

In this tutorial, you'll learn how to create a responsive and collapsible sidebar layout step-by-step by using and extending TailwindCSS grid classes.

Final Output: #

First, let me show you the final outputs on desktop and moble:

Desktop

NextJS Tailwind Collapsible Sidebar Desktop

Mobile

NextJS Tailwind Collapsible Sidebar Mobile

Tutorial Objectives: #

  1. Learn how to create a collapsible react sidebar from scratch using Tailwind CSS in a NextJS Project. (Note: if you're using create-react-app or similar, just be sure to install TailwindCSS beforehand. This is not included in the scope of this tutorial).
  2. Use the /pages folder directory in NextJS. We won't be using Next13's /app folder since it's still in beta at the time of this writing.
  3. Use React version 18.x and NextJS version 13.x with Typescript

Step 0: Project Setup #

To skip the lengthy process of installing TailwindCSS and dependencies. I created a starter template that includes Tailwind CSS pre-installed. If you're using an existing project, make sure to install TailwindCSS in NextJS.

Run the following command to create a new NextJS project using the starter template:

npx create-next-app -e https://github.com/jmarioste/next-tailwind-starter-2

Also, don't forget to install the TailwindCSS VSCode extension to get the intellisense going.

Step 1: Installing Dependencies #

We're going to install the heroicons package for the navigation icons, and the classnames package to easily manage the tailwind classes. Run the command to install both packages at once:

yarn add @heroicons/react classnames

The classnames package is a small library that allows us to easily organize and toggle classes depending on the application state. It also makes the code a lot more readable. I highly recommend using it when using TailwindCSS.

Step 2: Extending Tailwind Classes #

In this tutorial, we're going to use 300px as the width of the sidebar and 64px when collapsed. The grid-cols-* classes do not have a specific class for that so we're going to add it in tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      gridTemplateColumns: {
        sidebar: "300px auto", //for sidebar layout
        "sidebar-collapsed": "64px auto", //for collapsed sidebar layout
      },
    },
  },
};
12345678910111213141516

Now we get two additional classes (grid-cols-sidebar and grid-cols-sidebar-collapsed) from tailwind that easily splits the screen into two parts with the exact widths that we want. We'll use this when creating the layout component in the next step. 

Step 3: Creating The <Layout/> Component #

Next, let's create the Layout component and add it to _app.tsx

Create the file components/Layout.tsx and copy the code below:

// components/Layout.tsx
import classNames from "classnames";
import React, { PropsWithChildren, useState } from "react";
import { Bars3Icon } from "@heroicons/react/24/outline";
const Layout = (props: PropsWithChildren) => {
  const [collapsed, setSidebarCollapsed] = useState(false);
  return (
    <div
      className={classNames({
        // 👇 use grid layout
        "grid min-h-screen": true,
        // 👇 toggle the width of the sidebar depending on the state
        "grid-cols-sidebar": !collapsed,
        "grid-cols-sidebar-collapsed": collapsed,
        // 👇 transition animation classes
        "transition-[grid-template-columns] duration-300 ease-in-out": true,
      })}
    >
      {/* sidebar */}
      <div className="bg-indigo-700 text-white">
        <button onClick={() => setSidebarCollapsed((prev) => !prev)}>
          <Bars3Icon className="w-10 h-10" />
        </button>
      </div>
      {/* content */}
      <div className=""> {props.children}</div>
    </div>
  );
};
export default Layout;
123456789101112131415161718192021222324252627282930

Explanation:

We use grid layout and grid-cols-* classes to modify the width of our sidebar.

We use transition classes to add a simple smooth animation.

We use the collapsed state to toggle the width of our sidebar.

Output after this step:

NextJS/React Tawilind Collapsible Sidebar - Step 3 Output

Step 4: Creating the Sidebar Component #

Now that our layout is done, we just have to fill the sidebar component on the left. The sidebar is composed of 3 parts: The logo, the navigation items, and the user profile.

First, create the file components/Sidebar.tsx.

Step 4.1: Creating the Logo Part

The logo part consists of the logo and the button to control the sidebar. Examine the code with comments below. If you're not familiar with the class, you can always refer to the tailwind docs.

// components/Sidebar.tsx
import React from "react";
import cn from "classnames";
import {
  ChevronDoubleLeftIcon,
  ChevronDoubleRightIcon,
} from "@heroicons/react/24/outline";
// 👇 props to get and set the collapsed state from parent component
type Props = {
  collapsed: boolean;
  setCollapsed(collapsed: boolean): void;
};
const Sidebar = ({ collapsed, setCollapsed }: Props) => {
  // 👇 use the correct icon depending on the state.
  const Icon = collapsed ? ChevronDoubleRightIcon : ChevronDoubleLeftIcon;
  return (
    <div
      className={cn({
        "bg-indigo-700 text-zinc-50 z-20": true,
      })}
    >
      <div
        className={cn({
          "flex flex-col justify-between": true,
        })}
      >
        {/* logo and collapse button */}
        <div
          className={cn({
            "flex items-center border-b border-b-indigo-800": true,
            "p-4 justify-between": !collapsed,
            "py-4 justify-center": collapsed,
          })}
        >
          {!collapsed && <span className="whitespace-nowrap">My Logo</span>}
          <button
            className={cn({
              "grid place-content-center": true, // position
              "hover:bg-indigo-800 ": true, // colors
              "w-10 h-10 rounded-full": true, // shape
            })}
            // 👇 set the collapsed state on click
            onClick={() => setCollapsed(!collapsed)}
          >
            <Icon className="w-5 h-5" />
          </button>
        </div>
      </div>
    </div>
  );
};
export default Sidebar;

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152

Next, is to modify Layout.tsx and use the Sidebar component:

// components/Layout.tsx
import classNames from "classnames";
import React, { PropsWithChildren, useState } from "react";
import Sidebar from "./Sidebar";
const Layout = (props: PropsWithChildren) => {
  const [collapsed, setSidebarCollapsed] = useState(false);
  return (
    <div
      className={classNames({
        // ...omitted for brevity
      })}
    >
      {/* sidebar */}
      <Sidebar
        collapsed={collapsed}
        setCollapsed={() => setSidebarCollapsed((prev) => !prev)}
      />
      {/* content */}
      <div className=""> {props.children}</div>
    </div>
  );
};
export default Layout;
1234567891011121314151617181920212223

After this step, you get this result. Not bad, looks good already:

Step 4-1 Sidebar with Logo and Button

Step 4.2: Creating the Profile Part

At this point, we've gone through the important parts for this component. So you can actually skip this step and proceed to Step 5. You can still access the full code on GitHub.

If you want to follow along, add the following code to your Sidebar.tsx. I've highlighted the modified lines :

import React from "react";
import cn from "classnames";
import {
  ChevronDoubleLeftIcon,
  ChevronDoubleRightIcon,
} from "@heroicons/react/24/outline";
import Image from "next/image";
import Link from "next/link";
// 👇 props to get and set the collapsed state from parent component
type Props = {
  collapsed: boolean;
  setCollapsed(collapsed: boolean): void;
};
const Sidebar = ({ collapsed, setCollapsed }: Props) => {
  // 👇 use the correct icon depending on the state.
  const Icon = collapsed ? ChevronDoubleRightIcon : ChevronDoubleLeftIcon;
  return (
    <div
      className={cn({
        "bg-indigo-700 text-zinc-50 z-20": true,
      })}
    >
      <div
        className={cn({
          "flex flex-col justify-between": true,
          "h-full": true,
        })}
      >
        {/* logo and collapse button */}
        ...
        <div
          className={cn({
            "grid place-content-stretch p-4": true,
          })}
        >
          <div className="flex gap-4 items-center h-11 overflow-hidden">
            <Image
              src={"https://via.placeholder.com/150"}
              height={36}
              width={36}
              alt="profile image"
              className="rounded-full"
            />
            {!collapsed && (
              <div className="flex flex-col">
                <span className="text-indigo-50 my-0">Tom Cook</span>
                <Link href="/" className="text-indigo-200 text-sm">
                  View Profile
                </Link>
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};
export default Sidebar;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758

Step 4.3 Adding the Navigation Items Part

Next, lets create the navigation items part. Let's create the data by first creating the file components/defaultNavItems.tsx:

// components/defaultNavItems.tsx
import React from "react";
import {
  CalendarIcon,
  FolderIcon,
  HomeIcon,
  UserGroupIcon,
} from "@heroicons/react/24/outline";
// define a NavItem prop
export type NavItem = {
  label: string;
  href: string;
  icon: React.ReactNode;
};
export const defaultNavItems: NavItem[] = [
  {
    label: "Dashboard",
    href: "/",
    icon: <HomeIcon className="w-6 h-6" />,
  },
  {
    label: "Team",
    href: "/team",
    icon: <UserGroupIcon className="w-6 h-6" />,
  },
  {
    label: "Projects",
    href: "/projects",
    icon: <FolderIcon className="w-6 h-6" />,
  },
  {
    label: "Calendar",
    href: "/calendar",
    icon: <CalendarIcon className="w-6 h-6" />,
  },
];
123456789101112131415161718192021222324252627282930313233343536

Next, let's modify our sidebar component to use these items. I've also highlighted the modified lines, note that some code are omitted for brevity:

// components/Sidebar.tsx
...
import { defaultNavItems, NavItem } from "./defaultNavItems";
// add NavItem prop to component prop
type Props = {
  collapsed: boolean;
  navItems?: NavItem[];
  setCollapsed(collapsed: boolean): void;
};
const Sidebar = ({
  collapsed,
  navItems = defaultNavItems,
  setCollapsed,
}: Props) => {
  const Icon = collapsed ? ChevronDoubleRightIcon : ChevronDoubleLeftIcon;
  return (
    <div
      className={classNames({
        "bg-indigo-700 text-zinc-50 z-20": true,
        "transition-all duration-300 ease-in-out": true,
      })}
    >
      <div
        className={classNames({
          "flex flex-col justify-between ": true,
          "h-full": true,
        })}
      >
        {/* logo and collapse button */}
        {/* ...ommitted for brevity */}
        {/* nav items part */}
        <nav className="flex-grow">
          <ul
            className={classNames({
              "my-2 flex flex-col gap-2 items-stretch": true,
            })}
          >
            {navItems.map((item, index) => {
              return (
                <li
                  key={index}
                  className={classNames({
                    "text-indigo-100 hover:bg-indigo-900 flex": true, //colors
                    "transition-colors duration-300": true, //animation
                    "rounded-md p-2 mx-3 gap-4 ": !collapsed,
                    "rounded-full p-2 mx-3 w-10 h-10": collapsed,
                  })}
                >
                  <Link href={item.href} className="flex gap-2">
                    {item.icon} <span>{!collapsed && item.label}</span>
                  </Link>
                </li>
              );
            })}
          </ul>
        </nav>
        {/* profile part ...omitted for brevity */}
      </div>
    </div>
  );
};
export default Sidebar;
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

Explanation:

We use flex-grow so that it takes up the most height. Then, we iterate through the navItems and use the corresponding styles that you want. 

Step 4-3 Sidebar is completed

After this step, we've completed the sidebar but we still need to add responsive styles to make it look good on mobile.

Step 5: Creating Navbar Component #

Next, let's create a Navbar component that can hide or show the sidebar on mobile screens. Create the file components/Navbar.tsx:

// components/Navbar.tsx
import React from "react";
import { Bars3Icon } from "@heroicons/react/24/outline";
import classNames from "classnames";
type Props = {
  /**
   * Allows the parent component to modify the state when the
   * menu button is clicked.
   */
  onMenuButtonClick(): void;
};
const Navbar = (props: Props) => {
  return (
    <nav
      className={classNames({
        "bg-white text-zinc-500": true, // colors
        "flex items-center": true, // layout
        "w-screen md:w-full sticky z-10 px-4 shadow-sm h-[73px] top-0 ": true, //positioning & styling
      })}
    >
      <div className="font-bold text-lg">Admin Panel</div>
      <div className="flex-grow"></div>
      <button className="md:hidden" onClick={props.onMenuButtonClick}>
        <Bars3Icon className="h-6 w-6" />
      </button>
    </nav>
  );
};
export default Navbar;

1234567891011121314151617181920212223242526272829

Next, let's use it on Layout.tsx:

// components/Layout.tsx
import classNames from "classnames";
import React, { PropsWithChildren, useState } from "react";
import Navbar from "./Navbar";
import Sidebar from "./Sidebar";
const Layout = (props: PropsWithChildren) => {
  const [collapsed, setSidebarCollapsed] = useState(false);
  const [showSidebar, setShowSidebar] = useState(true);
  return (
    <div
      className={classNames({
        "grid bg-zinc-100 min-h-screen": true,
        "grid-cols-sidebar": !collapsed,
        "grid-cols-sidebar-collapsed": collapsed,
        "transition-[grid-template-columns] duration-300 ease-in-out": true,
      })}
    >
      <Sidebar collapsed={collapsed} setCollapsed={setSidebarCollapsed} />
      <div>
        <Navbar onMenuButtonClick={() => setShowSidebar((prev) => !prev)} />
        {props.children}
      </div>
    </div>
  );
};

export default Layout;
123456789101112131415161718192021222324252627

Explanation:

Line 8: We add another state showSidebar, to show or hide the sidebar.

Line 20: We toggle it when the menu button is clicked.

Step 6: Adding Show/Hide State to the <Sidebar/> #

Next is to add responsiveness to our Sidebar component. We'll have to target some responsive breakpoint classes as well as the show hide state from the Layout component.

Since we already have show/hide state in the Layout component, lets pass it to our sidebar component as props. First modify the sidebar component to accept show/hide state. I've again highlighted the modified parts:

// components/Sidebar.tsx
import React from "react";
import classNames from "classnames";
import Link from "next/link";
import Image from "next/image";
import { defaultNavItems, NavItem } from "./defaultNavItems";
import {
  ChevronDoubleLeftIcon,
  ChevronDoubleRightIcon,
} from "@heroicons/react/24/outline";
// add NavItem prop to component prop
type Props = {
  collapsed: boolean;
  navItems?: NavItem[];
  setCollapsed(collapsed: boolean): void;
  shown: boolean;
};
const Sidebar = ({
  collapsed,
  navItems = defaultNavItems,
  shown,
  setCollapsed,
}: Props) => {
  const Icon = collapsed ? ChevronDoubleRightIcon : ChevronDoubleLeftIcon;
  return (
    <div
      className={classNames({
        "bg-indigo-700 text-zinc-50 z-20": true,
        "transition-all duration-300 ease-in-out": true,
        "fixed md:static md:translate-x-0": true,
        "w-[300px]": !collapsed,
        "w-16": collapsed,
        "-translate-x-full": !shown,
        
      })}
    >
      {/* ... ommitted for brevity */}
    </div>
  );
};
export default Sidebar;
1234567891011121314151617181920212223242526272829303132333435363738394041

Explanation:

Lines 16 and 21: We add the shown state to later control the position of the sidebar.

Line 30: We use the fixed position on mobile, and change it back to static on md breakpoint. And since it's static on md, we always reset it to the initial position using translate-x-0 regardless of the state.

Line 31-32: Since the position is fixed, we have to set the width since it's already out of the grid layout at this point.

Line 33: We hide it to the left when it's not shown using -translate-x-full.

If you're having trouble understanding these classes, it's best to read the documentation on tailwind. and see what the specific css does.

Next, let's modify Layout.tsx to pass the shown state:

// components/Layout.tsx
import classNames from "classnames";
import React, { PropsWithChildren, useState } from "react";
import Navbar from "./Navbar";
import Sidebar from "./Sidebar";
const Layout = (props: PropsWithChildren) => {
  const [collapsed, setSidebarCollapsed] = useState(false);
  const [showSidebar, setShowSidebar] = useState(true);
  return (
    <div
      className={classNames({
        "grid bg-zinc-100 min-h-screen": true,
        "grid-cols-sidebar": !collapsed,
        "grid-cols-sidebar-collapsed": collapsed,
        "transition-[grid-template-columns] duration-300 ease-in-out": true,
      })}
    >
      <Sidebar
        collapsed={collapsed}
        setCollapsed={setSidebarCollapsed}
        shown={showSidebar}
      />
      <div className="">
        <Navbar onMenuButtonClick={() => setShowSidebar((prev) => !prev)} />
        {props.children}
      </div>
    </div>
  );
};
export default Layout;
123456789101112131415161718192021222324252627282930

That's it! After this step, you should have the same output as the final output above.

Full code and Demo #

The full code can be accessed at GitHub: Next Collapsible Sidebar Tailwind

The demo can be accessed at Stackblitz: Next Collapsible Sidebar Tailwind Demo

Conclusion #

We learned how to create a responsive collapsible sidebar from scratch just by using tailwind classes and a bit of react to handle the state. It's amazing how much control you have when using tailwind.

I think the main thing you need to think of when creating any component is, you have to have a plan. In this case, we started with a simple idea of using grid layout to control the width of the sidebar. We then added functionality later as we went on.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle