How you can create a sidebar like the daily.dev extension fron scratch using React and Tailwindcss in 4 steps

How you can create a sidebar like the daily.dev extension fron scratch using React and Tailwindcss in 4 steps

·

9 min read

Tailwind CSS is a utility-first CSS framework packed with classes that can be composed to build any design, directly in your markup.Next.js is a React framework with features like hybrid static and server rendering, TypeScript support, smart bundling, route pre-fetching, and more.

if you combine then both you can have incredible results and help you perform much more better.

In this post we will details everything you need to know to setup nextjs and tailwind css and make a cool sidebar.

if you like a video version

{% youtube pKt2-Iws_-A%} If you liked it don't forget to subscribe 🤓

Prerequisites

To complete this tutorial, you’ll need:

A local development environment for Node.js.

0 - Demo of the final result

you can see the final results here:

demo of a dasboard

1 - Setup Nextjs and Tailwind CSS

we can initiate our projet unsing npx

npx create-next-app sidebar

Let move to the sidebar directory

cd sidebar

we will need a couple of library to start coding our sidebar like framer motion for animation and react-icons for our icons

npm i react-icons framer-motion

now we have the packages we need to our project we can now setup tailwind to finish the setup process

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

we can activate tailwind and make it work in our project by simply editing the tailwind.config.js file.

Inside the module.export we simply need to add the path of our components and pages directory ( where we will use Tailwind CSS )

in our case, it’s a page folder since we will have only one page. we will have only one page.

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

the last step is to add a tailwind directive in our global css file

@tailwind base;
@tailwind components;
@tailwind utilities;

2 - Let Prepare everything

The first things is to import some icons and add the data for our sidebar

as we can see in the picture

if you look we have 3 parts

  • the main title
  • the icon and the text juste at the left

our data

import Head from 'next/head'
import {
  BsPlus,
  BsSearch,
  BsEyeFill,
  BsBookmarkFill,
  BsFillArrowLeftSquareFill,
  BsPeopleFill,
  BsTerminalFill,
  BsFillArrowRightSquareFill
} from 'react-icons/bs'

import { AiFillFire, AiFillMessage, } from 'react-icons/ai'
import { IoMdArrowRoundUp } from 'react-icons/io'
import { MdNightlightRound, MdFeedback } from 'react-icons/md'
import { FaCog } from 'react-icons/fa'

const data = [
  {
    name: 'Discover',
    items: [
      {
        title: 'Popular',
        icon: AiFillFire,
      },
      {
        title: 'Most Upvoted',
        icon: IoMdArrowRoundUp,
      },
      {
        title: 'Best Discussions',
        icon: AiFillMessage,
      },
      {
        title: 'Search',
        icon: BsSearch,
      },
    ]
  },
  {
    name: 'Manage',
    items: [
      {
        title: 'Bookmarks',
        icon: BsBookmarkFill,
      },
      {
        title: 'Reading history',
        icon: BsEyeFill,
      },
      {
        title: 'Focus Mode',
        icon: MdNightlightRound,
      },
      {
        title: 'Customize',
        icon: FaCog,
      },
    ]
  },
]

const datafooter = [

  {
    name: '',
    items: [
      {
        title: 'Docs',
        icon: BsBookmarkFill,
      },
      {
        title: 'Changelog',
        icon: BsTerminalFill,
      },
      {
        title: 'Feedback',
        icon: MdFeedback,
      },
      {
        title: 'Invite people',
        icon: BsPeopleFill,
      },
    ]
  },
]

//.... the body of the component

...

//the end

now we have all our data and icons we can now start coding

3 - Create the main container and card element

the first task is to add a left border to our sidebar and create the card element inside.

our second step is to add our collapse icon and make it appear only as we take a mouse and hover the container as you can see

demo of our card

to create that this is the code

//imports
...
//end imports

export default function Home() {

  return (
    <div className='min-h-screen bg-black' >

      <div className='max-w-[250px]  animate duration-300 border-r border-gray-700 relative flex flex-col py-10 min-h-screen group' >

        <BsFillArrowLeftSquareFill className='absolute hidden text-2xl text-white cursor-pointer -right-4 top-10 group-hover:block ' />

        <div className={` border-green-400 border shadow-green-400/60 shadow-lg rounded-lg px-4   max-w-[220px] h-[120px] flex justify-center mx-2  flex-col mb-4`} >
          <p className='font-thin text-white text-md' >
            Get the content you need by creating a personal feed
          </p>
          <button className='flex items-center justify-center w-full py-2 my-2 font-bold text-black bg-green-400 rounded-lg' >
            <BsPlus className='text-2xl' />
            <p  >
              Create me feed
            </p>
          </button>
        </div>

      </div>

    </div>
  )
}
//imports

...

//end imports

export default function Home() {

  return (
    <div className='min-h-screen bg-black' >

      <div className='max-w-[250px]  animate duration-300 border-r border-gray-700 relative flex flex-col py-10 min-h-screen group' >

        <BsFillArrowLeftSquareFill className='absolute hidden text-2xl text-white cursor-pointer -right-4 top-10 group-hover:block ' />

        <div className={` border-green-400 border shadow-green-400/60 shadow-lg rounded-lg px-4   max-w-[220px] h-[120px] flex justify-center mx-2  flex-col mb-4`} >
          <p className='font-thin text-white text-md' >
            Get the content you need by creating a personal feed
          </p>
          <button className='flex items-center justify-center w-full py-2 my-2 font-bold text-black bg-green-400 rounded-lg' >
            <BsPlus className='text-2xl' />
            <p  >
              Create me feed
            </p>
          </button>
        </div>

        <div className='grow'>
          {data.map((group, index) => (
            <div key={index} className='my-2' >
              <p className='mb-2 ml-4 text-sm font-bold text-gray-500' >{group.name}</p>

              {group.items.map((item, index2) => (
                <div key={index2} className='flex px-4 py-1 cursor-pointer' >
                  <item.icon className='text-lg text-gray-500' />
                  <p className='ml-4 text-sm font-bold text-gray-400' > {item.title}</p>
                </div>

              ))}
            </div>
          ))}
        </div>

        <div>
          {datafooter.map((group, index) => (
            <div key={index} className='my-2' >
              <p className='mb-2 ml-4 text-sm font-bold text-gray-500' >{group.name}</p>

              {group.items.map((item, index2) => (
                <div key={index2} className='flex px-4 py-1 cursor-pointer' >
                  <item.icon className='text-lg text-gray-500' />
                  <p className='ml-4 text-sm font-bold text-gray-400' > {item.title}</p>
                </div>

              ))}
            </div>
          ))}
        </div>

      </div>

    </div>
  )
}

main container and card

4 - Animation

we can now start adding a little bit of animation to our project using framer motion

the first things is to import motion and useAnimation that into our code

//...import

import { useState, useEffect } from 'react'
import { motion, useAnimation } from 'framer-motion'

//
...
//

we have three kind of animation here

the fisrt one is the sidebar container.We need to animation the width according to the state of our sidebar if it’s open or not

the second is to make our text visible and invisible and then remove or show then to the DOM with the display property

the last animation is to simple animate the opacity of our items title we don't want to make our design jump every time we open and close the sidebar

we know need to define the famer motion controllers and translate all the details above into our animation.


export default function Home() {

  const [active, setActive] = useState(false)
  const controls = useAnimation()
  const controlText = useAnimation()
  const controlTitleText = useAnimation()

  const showMore = () => {
    controls.start({
      width: '250px',
      transition: { duration: 0.001 }
    })
    controlText.start({
      opacity: 1,
      display: 'block',
      transition: {delay:0.3}
    })
    controlTitleText.start({
      opacity: 1,
      transition: {delay:0.3}
    })

    setActive(true)
  }

  const showLess = () => {
    controls.start({
      width: '55px',
      transition: { duration: 0.001 }
    })

    controlText.start({
      opacity: 0,
      display: 'none',
    })

    controlTitleText.start({
      opacity: 0,
    })

    setActive(false)

  }
//main part 
...

//end main part

}

once we declared our animation we can use it on our page


export default function Home() {

  const [active, setActive] = useState(false)
  const controls = useAnimation()
  const controlText = useAnimation()
  const controlTitleText = useAnimation()

  const showMore = () => {
    controls.start({
      width: '250px',
      transition: { duration: 0.001 }
    })
    controlText.start({
      opacity: 1,
      display: 'block',
      transition: {delay:0.3}
    })
    controlTitleText.start({
      opacity: 1,
      transition: {delay:0.3}
    })

    setActive(true)
  }

  const showLess = () => {
    controls.start({
      width: '55px',
      transition: { duration: 0.001 }
    })

    controlText.start({
      opacity: 0,
      display: 'none',
    })

    controlTitleText.start({
      opacity: 0,
    })

    setActive(false)

  }

  useEffect(() => {
    showMore()
  },[])

  return (
    <div className='min-h-screen bg-black' >
      <Head>
        <title>Sidebar</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <motion.div animate={controls} className='max-w-[250px]  animate duration-300 border-r border-gray-700 relative flex flex-col py-10 min-h-screen group' >

        {active && <BsFillArrowLeftSquareFill onClick={showLess} className='absolute hidden text-2xl text-white cursor-pointer -right-4 top-10 group-hover:block ' />}
        {!active && <BsFillArrowRightSquareFill onClick={showMore} className='absolute text-2xl text-white cursor-pointer -right-4 top-10' />}

        <div className={`${active && 'border-green-400 border shadow-green-400/60 shadow-lg rounded-lg px-4'}   max-w-[220px] h-[120px] flex justify-center mx-2  flex-col mb-4`} >
          <motion.p animate={controlText} className='font-thin text-white text-md' >
            Get the content you need by creating a personal feed
          </motion.p>
          <button className='flex items-center justify-center w-full py-2 my-2 font-bold text-black bg-green-400 rounded-lg' >
            <BsPlus className='text-2xl' />
            <motion.p animate={controlText}  >
              Create me feed
            </motion.p>
          </button>
        </div>

        <div className='grow'>
          {data.map((group, index) => (
            <div key={index} className='my-2' >
              <motion.p animate={controlTitleText} className='mb-2 ml-4 text-sm font-bold text-gray-500' >{group.name}</motion.p>

              {group.items.map((item, index2) => (
                <div key={index2} className='flex px-4 py-1 cursor-pointer' >
                  <item.icon className='text-lg text-gray-500' />
                  <motion.p animate={controlText} className='ml-4 text-sm font-bold text-gray-400' > {item.title}</motion.p>
                </div>

              ))}
            </div>
          ))}
        </div>

        <div>
          {datafooter.map((group, index) => (
            <div key={index} className='my-2' >
              <motion.p animate={controlTitleText} className='mb-2 ml-4 text-sm font-bold text-gray-500' >{group.name}</motion.p>

              {group.items.map((item, index2) => (
                <div key={index2} className='flex px-4 py-1 cursor-pointer' >
                  <item.icon className='text-lg text-gray-500' />
                  <motion.p animate={controlText} className='ml-4 text-sm font-bold text-gray-400' > {item.title}</motion.p>
                </div>

              ))}
            </div>
          ))}
        </div>

      </motion.div>

    </div>
  )
}

the final code of our page

import Head from 'next/head'
import { useState, useEffect } from 'react'
import {
  BsPlus,
  BsSearch,
  BsEyeFill,
  BsBookmarkFill,
  BsFillArrowLeftSquareFill,
  BsPeopleFill,
  BsTerminalFill,
  BsFillArrowRightSquareFill
} from 'react-icons/bs'

import { AiFillFire, AiFillMessage, } from 'react-icons/ai'
import { IoMdArrowRoundUp } from 'react-icons/io'
import { MdNightlightRound, MdFeedback } from 'react-icons/md'
import { FaCog } from 'react-icons/fa'

import { motion, useAnimation } from 'framer-motion'

const data = [
  {
    name: 'Discover',
    items: [
      {
        title: 'Popular',
        icon: AiFillFire,
      },
      {
        title: 'Most Upvoted',
        icon: IoMdArrowRoundUp,
      },
      {
        title: 'Best Discussions',
        icon: AiFillMessage,
      },
      {
        title: 'Search',
        icon: BsSearch,
      },
    ]
  },
  {
    name: 'Manage',
    items: [
      {
        title: 'Bookmarks',
        icon: BsBookmarkFill,
      },
      {
        title: 'Reading history',
        icon: BsEyeFill,
      },
      {
        title: 'Focus Mode',
        icon: MdNightlightRound,
      },
      {
        title: 'Customize',
        icon: FaCog,
      },
    ]
  },
]

const datafooter = [

  {
    name: '',
    items: [
      {
        title: 'Docs',
        icon: BsBookmarkFill,
      },
      {
        title: 'Changelog',
        icon: BsTerminalFill,
      },
      {
        title: 'Feedback',
        icon: MdFeedback,
      },
      {
        title: 'Invite people',
        icon: BsPeopleFill,
      },
    ]
  },
]

export default function Home() {

  const [active, setActive] = useState(false)
  const controls = useAnimation()
  const controlText = useAnimation()
  const controlTitleText = useAnimation()

  const showMore = () => {
    controls.start({
      width: '250px',
      transition: { duration: 0.001 }
    })
    controlText.start({
      opacity: 1,
      display: 'block',
      transition: {delay:0.3}
    })
    controlTitleText.start({
      opacity: 1,
      transition: {delay:0.3}
    })

    setActive(true)
  }

  const showLess = () => {
    controls.start({
      width: '55px',
      transition: { duration: 0.001 }
    })

    controlText.start({
      opacity: 0,
      display: 'none',
    })

    controlTitleText.start({
      opacity: 0,
    })

    setActive(false)

  }

  useEffect(() => {
    showMore()
  },[])

  return (
    <div className='min-h-screen bg-black' >
      <Head>
        <title>Sidebar</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <motion.div animate={controls} className='max-w-[250px]  animate duration-300 border-r border-gray-700 relative flex flex-col py-10 min-h-screen group' >

        {active && <BsFillArrowLeftSquareFill onClick={showLess} className='absolute hidden text-2xl text-white cursor-pointer -right-4 top-10 group-hover:block ' />}
        {!active && <BsFillArrowRightSquareFill onClick={showMore} className='absolute text-2xl text-white cursor-pointer -right-4 top-10' />}

        <div className={`${active && 'border-green-400 border shadow-green-400/60 shadow-lg rounded-lg px-4'}   max-w-[220px] h-[120px] flex justify-center mx-2  flex-col mb-4`} >
          <motion.p animate={controlText} className='font-thin text-white text-md' >
            Get the content you need by creating a personal feed
          </motion.p>
          <button className='flex items-center justify-center w-full py-2 my-2 font-bold text-black bg-green-400 rounded-lg' >
            <BsPlus className='text-2xl' />
            <motion.p animate={controlText}  >
              Create me feed
            </motion.p>
          </button>
        </div>

        <div className='grow'>
          {data.map((group, index) => (
            <div key={index} className='my-2' >
              <motion.p animate={controlTitleText} className='mb-2 ml-4 text-sm font-bold text-gray-500' >{group.name}</motion.p>

              {group.items.map((item, index2) => (
                <div key={index2} className='flex px-4 py-1 cursor-pointer' >
                  <item.icon className='text-lg text-gray-500' />
                  <motion.p animate={controlText} className='ml-4 text-sm font-bold text-gray-400' > {item.title}</motion.p>
                </div>

              ))}
            </div>
          ))}
        </div>

        <div>
          {datafooter.map((group, index) => (
            <div key={index} className='my-2' >
              <motion.p animate={controlTitleText} className='mb-2 ml-4 text-sm font-bold text-gray-500' >{group.name}</motion.p>

              {group.items.map((item, index2) => (
                <div key={index2} className='flex px-4 py-1 cursor-pointer' >
                  <item.icon className='text-lg text-gray-500' />
                  <motion.p animate={controlText} className='ml-4 text-sm font-bold text-gray-400' > {item.title}</motion.p>
                </div>

              ))}
            </div>
          ))}
        </div>

      </motion.div>

    </div>
  )
}

you can see the final result compared to the one we want to clone

we did it. Thank for reading. You can find the code on my github.