How you can create a sidebar like the daily.dev extension fron scratch using React and Tailwindcss in 4 steps
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
% 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:
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
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
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>
)
}
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.