Designly Blog

Create a Responsive Animated Sidebar Using React / Next.js and Tailwind CSS

Create a Responsive Animated Sidebar Using React / Next.js and Tailwind CSS

Posted in Front End Development by Jay Simons
Published on March 26, 2023

Most of the design systems, such as Material UI and Ant Design have drawer-like elements that allow you to create animated sliding sidebars, but many, myself included, find these design systems to be limiting. They also introduce a ton of overhead in dependency code base.

During any web development project, I am always looking for places where layers of abstraction and dependencies can be eliminated, especially when the underlying task that the layer of abstraction is producing is relatively simple. Well, good news: a responsive sidebar is one of those things! 🤗

In this tutorial, I'll show you how to leverage the power of Tailwind CSS and Next.js to create a simple yet elegant mobile-responsive sidebar using no other dependencies. (I am using react-icons for this example, but you can use whatever you like.)


The first thing we'll want to do is create a folder called Layout in our components folder. This folder will house all the components required to render our sidebar, mobile menu bar and the main layout component.

Next, let's create our sidebar component:

// @/components/Layout/Sidebar.js
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'

import { SlHome } from 'react-icons/sl'
import { BsInfoSquare, BsEnvelopeAt } from 'react-icons/bs'
import { FaTshirt, FaRedhat } from 'react-icons/fa'

import logo from '@/img/logo.svg'

export default function Sidebar({ show, setter }) {
    const router = useRouter();

    // Define our base class
    const className = "bg-black w-[250px] transition-[margin-left] ease-in-out duration-500 fixed md:static top-0 bottom-0 left-0 z-40";
    // Append class based on state of sidebar visiblity
    const appendClass = show ? " ml-0" : " ml-[-250px] md:ml-0";

    // Clickable menu items
    const MenuItem = ({ icon, name, route }) => {
        // Highlight menu item based on currently displayed route
        const colorClass = router.pathname === route ? "text-white" : "text-white/50 hover:text-white";

        return (
            <Link
                href={route}
                onClick={() => {
                    setter(oldVal => !oldVal);
                }}
                className={`flex gap-1 [&>*]:my-auto text-md pl-6 py-3 border-b-[1px] border-b-white/10 ${colorClass}`}
            >
                <div className="text-xl flex [&>*]:mx-auto w-[30px]">
                    {icon}
                </div>
                <div>{name}</div>
            </Link>
        )
    }

    // Overlay to prevent clicks in background, also serves as our close button
    const ModalOverlay = () => (
        <div
            className={`flex md:hidden fixed top-0 right-0 bottom-0 left-0 bg-black/50 z-30`}
            onClick={() => {
                setter(oldVal => !oldVal);
            }}
        />
    )

    return (
        <>
            <div className={`${className}${appendClass}`}>
                <div className="p-2 flex">
                    <Link href="/">
                        {/*eslint-disable-next-line*/}
                        <img src={logo.src} alt="Company Logo" width={300} height={300} />
                    </Link>
                </div>
                <div className="flex flex-col">
                    <MenuItem
                        name="Home"
                        route="/"
                        icon={<SlHome />}
                    />
                    <MenuItem
                        name="T-Shirts"
                        route="/t-shirts"
                        icon={<FaTshirt />}
                    />
                    <MenuItem
                        name="Hats"
                        route="/hats"
                        icon={<FaRedhat />}
                    />
                    <MenuItem
                        name="About Us"
                        route="/about"
                        icon={<BsInfoSquare />}
                    />
                    <MenuItem
                        name="Contact"
                        route="/contact"
                        icon={<BsEnvelopeAt />}
                    />
                </div>
            </div>
            {show ? <ModalOverlay /> : <></>}
        </>
    )
}

Ok, let's break this down. This component receives two props: show and setter. These are passed from the HOC Layout, which we will define later. The show prop is a boolean variable that toggles the state of the sidebar's visibility. The setter prop is the function to set that state.

Next we define our base class and another variable called appendClass, which will be conditionally rendered based on our show state variable. When show is false, we set the left margin to -250px but we set it to 0 on medium screens or larger. We also set the transition property in our base class to a custom value of [margin-left], which will cause the browser to automagically animate our sidebar as a slide animation! 😁

Then we define a subcomponent called <MenuItem> which will be used for our links. We use next/router to determine which link we should highlight as active. Also, we toggle the setter function when a menu item is clicked to the sidebar closes. This is required because this is a single-page app, and it would be annoying if the sidebar stayed open when you click a link.

Finally, we have <ModalOverlay>, which serves two purposes: 1.) it prevents clicking of anything in the background while the sidebar is open, and 2.) it serves as the close button for the sidebar. You could also add an additional visible close button as well, but I think that touching in the outside bound areas of modals on mobile UIs is a very intuitive and natural function, so I opted not to include one.


Ok, now we'll create our mobile menu bar component that will only displayed on screens smaller than the medium breakpoint:

// @/components/Layout/MenuBarMobile.js
import React from 'react'
import Link from 'next/link'
import { FiMenu as Icon } from 'react-icons/fi'
import { FaUser } from 'react-icons/fa'

import logo from '@/img/logo.svg'

export default function MenuBarMobile({ setter }) {
    return (
        <nav className="md:hidden z-20 fixed top-0 left-0 right-0 h-[60px] bg-black flex [&>*]:my-auto px-2">
            <button
                className="text-4xl flex text-white"
                onClick={() => {
                    setter(oldVal => !oldVal);
                }}
            >
                <Icon />
            </button>
            <Link href="/" className="mx-auto">
                {/*eslint-disable-next-line*/}
                <img
                    src={logo.src}
                    alt="Company Logo"
                    width={50}
                    height={50}
                />
            </Link>
            <Link
                className="text-3xl flex text-white"
                href="/login"
            >
                <FaUser />
            </Link>
        </nav>
    )
}

This component has a fixed position at the top of the screen on mobile devices only. It serves primarily as a vehicle for our sidebar opener button. I put the login button there simply for illustration and symmetry. We need only to pass the setter function of our showSidebar state to this component.


Lastly, let's create our Layout component that will bring it all together:

// @/components/Layout/index.js
import React, { useState } from 'react'
import Head from 'next/head'
import Sidebar from './Sidebar';
import MenuBarMobile from './MenuBarMobile';

export default function Layout({ pageTitle, children }) {
    // Concatenate page title (if exists) to site title
    let titleConcat = "Responsive Sidebar Example";
    if (pageTitle) titleConcat = pageTitle + " | " + titleConcat;

    // Mobile sidebar visibility state
    const [showSidebar, setShowSidebar] = useState(false);

    return (
        <>
            <Head>
                <title>{titleConcat}</title>
            </Head>
            <div className="min-h-screen">
                <div className="flex">
                    <MenuBarMobile setter={setShowSidebar} />
                    <Sidebar show={showSidebar} setter={setShowSidebar} />
                    <div className="flex flex-col flex-grow w-screen md:w-full min-h-screen">
                        {children}
                    </div>
                </div>
            </div>
        </>
    )
}

As you can see, this is where we define our showSidebar state variable. We then pass it to <MenuBarMobile> and <Sidebar>.


Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the Designly Blog. Also, please leave your comments! I love to hear thoughts from my readers.

Looking for a web developer? I'm available for hire! To inquire, please fill out a contact form.


Resources for this tutorial:

  1. GitHub Repo
  2. Demo Page

Loading comments...