ReactHustle

How to Create a Responsive Tailwind Sidebar Layout in NextJS 12

Jasser Mark Arioste

Jasser Mark Arioste

How to Create a Responsive Tailwind Sidebar Layout in NextJS 12

Hello, hustlers! In this tutorial, you'll learn how to create a beautiful responsive and sticky sidebar in NextJS using TailwindCSS, React and Typescript.

Introduction #

Recently, I tried creating a sidebar in react-bootstrap but it was really a pain to customize. The problem is that in bootstrap they only have the horizontal navbar. If you decide to use it, you'll have to target specific CSS hierarchies to change the style, and you'll have to integrate it into NextJS. If you don't use it, might as well not use bootstrap at all.

All right, I thought that this would take a lot of time so I decided to use TailwindCSS instead. By using TailwindCSS, we have complete control over the elements. And since we use React to handle the states, we don't have to use 3rd-party libraries where we have little control over the behavior. It's also much easier to integrate it into NextJS since we only use HTML and CSS.

Final Output #

Here's the final output for the responsive sidebar component. It's an attempt to clone sidebar layouts from TailwindUI.

Desktop Layout

NextJS tailwind sidebar tutorial final output on the desktop

The sidebar is fixed on the desktop version.

Mobile Layout

Here's the final output for the mobile version

NextJS tailwind sidebar tutorial final output on mobile

This seems simple, but it's quite tricky since on mobile the sidebar is on top of everything. We'll also need to adjust its height on desktop and the grid layout for different viewports. In this tutorial, we'll see how to solve this problem.

Now that we know everything we need to know. Let's proceed with the implementation!

Step 0 - Project Setup #

First, let's set up our project by creating a new NextJS app.

npx create-next-app --ts nextjs-tailwind-sidebar

Next, let's install Tailwind CSS and all the other dependencies that we'll be needing in this tutorial. Run the commands:

# yarn
cd nextjs-tailwind-sidebar
yarn add -D tailwindcss postcss autoprefixer
yarn add @heroicons/react classnames usehooks-ts

#npm
cd nextjs-tailwind-sidebar
npm i -D tailwindcss postcss autoprefixer
npm i @heroicons/react classnames usehooks-ts

We'll use @heroicons/react for the sidebar icons, classnames package for organizing the tailwind classes and readability, and usehooks-ts for implementing click-outside functionality.

Next, let's quickly set up TailwindCSS for NextJS. Run the following command:

npx tailwindcss init -p

Next, configure tailwind.config.js and copy the following code:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
123456789101112

Next, modify styles/globals.css file and add the tailwind directives:

# styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
1234

Step 1 - Creating the Layout Component #

In this tutorial, we'll be using the grid CSS layout in our components. First, let's modify tailwind.config.js. We'll add two new classes: grid-cols-* and grid-rows-*. Since we need specific pixels for the navbar height and sidebar width, we can't rely on the default utility classes on tailwind.

/** @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. adds grid-cols-sidebar class
      }, 
      gridTemplateRows: {
        header: "64px auto", // 👈 for the navbar layout. adds grid-rows-header class
      },
    },
  },
};
123456789101112131415161718

If you installed the TailwindCSS extension in VSCode, you should see grid-cols-sidebar and grid-rows-header classes on IntelliSense:

Next, let's create a Layout component. Create the file components/layout/Layout.tsx:

// components/layout/Layout.tsx
import React, { PropsWithChildren } from "react";
const Layout = (props: PropsWithChildren) => {
  return (
    <div className="grid min-h-screen grid-rows-header bg-zinc-100">
      <div>Navbar</div>
      <div className="grid md:grid-cols-sidebar">
        <div>Sidebar</div>
        {props.children}
      </div>
    </div>
  );
};

export default Layout;
123456789101112131415

It uses the two classes we made earlier. For grid-cols-sidebar class, we only use it on the medium breakpoints and up. We don't need it on small devices since we'll use the position:fixed style on small devices. 

Now, let's use it on pages/_app.tsx:

// pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Layout from "components/layout/Layout";
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />;
    </Layout>
  );
}
export default MyApp;
123456789101112

After this step, It looks like this on desktop:

Next Responsive Tailwind Sidebar - After Step 1

If you check the output on mobile devices, it still looks terrible. We'll fix this later once we create the next components.

Step 2 - Creating the Navbar Component #

Next, we'll create a simple Navbar/Header component. that has a menu button to hide/show the sidebar on mobile devices:

Create the file components/layout/Navbar.tsx:

// components/layout/Navbar.tsx
import React from "react";
import { Bars3Icon } from "@heroicons/react/24/outline";
import classNames from "classnames";
type Props = {
  onMenuButtonClick(): void;
};
const Navbar = (props: Props) => {
  return (
    <nav
      className={classNames({
        "bg-white text-zinc-500": true, // colors
        "flex items-center": true, // layout
        "w-full fixed z-10 px-4 shadow-sm h-16": true, //positioning & styling
      })}
    >
      <div className="font-bold text-lg">My Logo</div>
      <div className="flex-grow"></div> {/** spacer */}
      <button className="md:hidden" onClick={props.onMenuButtonClick}>
        <Bars3Icon className="h-6 w-6" />
      </button>
    </nav>
  );
};
export default Navbar;

12345678910111213141516171819202122232425

We just style the navbar according to our preferences and add a click handler for the menu button to toggle the sidebar. Let's use it on the <Layout/> component:

import React, { PropsWithChildren, useState } from "react";
import Navbar from "./Navbar";
const Layout = (props: PropsWithChildren) => {
  const [showSidebar, setShowSidebar] = useState(false);
  return (
    <div className="grid min-h-screen grid-rows-header bg-zinc-100">
      <div className="bg-white shadow-sm z-10">
        <Navbar onMenuButtonClick={() => setShowSidebar((prev) => !prev)} />
      </div>

      <div className="grid md:grid-cols-sidebar ">
        <div className="shadow-md bg-zinc-50">Sidebar</div>
        {props.children}
      </div>
    </div>
  );
};
export default Layout;
123456789101112131415161718

After this step, you'll have this output on desktop:

Next Tailwind Sidebar after step 2 - Desktop

And on mobile you'll see a menu button that doesn't do anything for now:

Next Tailwind Sidebar after step 2 - Mobile

Step 3 - Creating the Sidebar Component #

Next, let's create the <Sidebar/> component. First, we'll start with the bare minimum functionality. We'll also make it responsive on mobile and desktop, and make it hide/show on mobile. We'll add the links and other functionality later after this step.

Create the file component/layout/Sidebar.tsx:

import React from "react";
import classNames from "classnames";
type Props = {
  open: boolean;
  setOpen(open: boolean): void;
};
const Sidebar = ({ open, setOpen }: Props) => {
  return (
    <div
      className={classNames({
        "flex flex-col justify-between": true, // layout
        "bg-indigo-700 text-zinc-50": true, // colors
        "md:w-full md:sticky md:top-16 md:z-0 top-0 z-20 fixed": true, // positioning
        "md:h-[calc(100vh_-_64px)] h-full w-[300px]": true, // for height and width
        "transition-transform .3s ease-in-out md:translate-x-0": true, //animations
        "-translate-x-full ": !open, //hide sidebar to the left when closed
      })}
    >
      <nav className="md:sticky top-0 md:top-16">
        {/* nav items */}
        <ul className="py-2 flex flex-col gap-2">
          <li>links here</li>
        </ul>
      </nav>
    </div>
  );
};
export default Sidebar;
12345678910111213141516171819202122232425262728

Explanation:

Lines 4-5: Since the state is handled by the <Layout/> component, we define the props for the sidebar. We need the open state and setOpen function to hide/show the sidebar inside the <Sidebar/> component. 

Line 13: Notice that we use fixed as the default position for mobile devices. We use md:sticky for desktop devices. Also for mobile, we use z-20 (z-index:20) so that it's on top of the navbar.

Line 14: We adjust the height of the sidebar according to the screen size. For mobile, it takes full height.  For desktop devices, it takes full height minus 64px.

Lines 15-16: We use -translate-x-full to completely hide the sidebar to the left of the screen on mobile. If the sidebar is open, we remove this class. For desktop devices, we don't care about the state so we just set md:translate-x-0 to set it in place.

After this step, you got something like this on desktop devices, It looks the same as the previous step:

NextJS Tailwind Sidebar Step 3 Desktop

For mobile, the clicking on menu button now works:

Next tailwind responsive sidebar mobile hide/show

Step 5 - Adding click-outside behavior #

Next, we'll add click-outside behavior so that the sidebar closes when you click outside of it.

// components/layout/Sidebar.tsx
import React, { useRef } from "react";
import classNames from "classnames";
import { useOnClickOutside } from "usehooks-ts";
type Props = {
  open: boolean;
  setOpen(open: boolean): void;
};
const Sidebar = ({ open, setOpen }: Props) => {
  const ref = useRef<HTMLDivElement>(null);
  useOnClickOutside(ref, (e) => {
    setOpen(false);
  });
  return (
    <div
      className={classNames({
        // ... ommitted for brevity
      })}
      ref={ref}
    >
      <nav className="md:sticky top-0 md:top-16">
        {/* nav items */}
        <ul className="py-2 flex flex-col gap-2">
          <li>Nav items</li>
        </ul>
      </nav>
    </div>
  );
};
export default Sidebar;
123456789101112131415161718192021222324252627282930

Explanation:

Line 4: We import the useOnClickOutside hook by usehooks-ts

Lines 10-13: We define a ref that specifies the element boundary, and we add the handler to hide the sidebar.

Line 19: We attach the ref to the root sidebar div.

Step 6 - Decorating the Sidebar  component #

Now we can add the navigation items and decorate the sidebar since we're done with the core functionality. Here's the full code for the sidebar:

// components/layout/Sidebar.tsx
import React, { useRef } from "react";
import classNames from "classnames";
import Link from "next/link";
import Image from "next/image";
import { defaultNavItems } from "./defaultNavItems";
import { useOnClickOutside } from "usehooks-ts";
// define a NavItem prop
export type NavItem = {
  label: string;
  href: string;
  icon: React.ReactNode;
};
// add NavItem prop to component prop
type Props = {
  open: boolean;
  navItems?: NavItem[];
  setOpen(open: boolean): void;
};
const Sidebar = ({ open, navItems = defaultNavItems, setOpen }: Props) => {
  const ref = useRef<HTMLDivElement>(null);
  useOnClickOutside(ref, (e) => {
    setOpen(false);
  });
  return (
    <div
      className={classNames({
        "flex flex-col justify-between": true, // layout
        "bg-indigo-700 text-zinc-50": true, // colors
        "md:w-full md:sticky md:top-16 md:z-0 top-0 z-20 fixed": true, // positioning
        "md:h-[calc(100vh_-_64px)] h-full w-[300px]": true, // for height and width
        "transition-transform .3s ease-in-out md:-translate-x-0": true, //animations
        "-translate-x-full ": !open, //hide sidebar to the left when closed
      })}
      ref={ref}
    >

      <nav className="md:sticky top-0 md:top-16">
        {/* nav items */}
        <ul className="py-2 flex flex-col gap-2">
          {navItems.map((item, index) => {
            return (
              <Link key={index} href={item.href}>
                <li
                  className={classNames({
                    "text-indigo-100 hover:bg-indigo-900": true, //colors
                    "flex gap-4 items-center ": true, //layout
                    "transition-colors duration-300": true, //animation
                    "rounded-md p-2 mx-2": true, //self style
                  })}
                >
                  {item.icon} {item.label}
                </li>
              </Link>
            );
          })}
        </ul>
      </nav>
      {/* account  */}
      <div className="border-t border-t-indigo-800 p-4">
        <div className="flex gap-4 items-center">
          <Image
            src={
              "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
            }
            height={36}
            width={36}
            alt="profile image"
            className="rounded-full"
          />
          <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>
  );
};
export default Sidebar;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182

For the navigation items, create the file components/layout/defaultNavItems.tsx and copy the following code:

// components/layout/defaultNavItems.tsx
import React from "react";
import {
  CalendarIcon,
  FolderIcon,
  HomeIcon,
  UserGroupIcon,
} from "@heroicons/react/24/outline";
import { NavItem } from "./Sidebar";

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" />,
  },
];
1234567891011121314151617181920212223242526272829303132

After this step you'll get the beautiful sidebar component:

NextJS Tailwind Responsive Sidebar Final output.

That's basically it!

Full Code and Demo #

If you'd like to get the full code, it's available on GitHub: https://github.com/jmarioste/next-responsive-sidebar-tailwind

The demo is available on Stackblitz: NextJS Responsive Tailwind Sidebar Tutorial

Conclusion #

We learned how to create a beautiful responsive sidebar from scratch using tailwind CSS in NextJS. I think learning how to create components from scratch is a really good exercise for every developer.

In my opinion, I think using TailwindCSS is like a cheat code that allows you to add responsive styling to components at will. This is really hard to do in other CSS solutions like plain CSS, SCSS, styled-components, or opinionated libraries like bootstrap. 

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.

Credits: Image by Alexandru Manole from Pixabay

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