Sidebars are perhaps the most ubiquitous components in modern web app development. Almost every app built today requires it in some capacity. This applies to applications as complex as analytics dashboards to common applications like user-facing SaaS dashboard.
If that's the case, why not opt for a plug-and-play solution like react-sidenav? It's simpler and faster than rolling out your own solution, after all.
All it takes is reading the documentation, tinkering around with the library and hey-presto - a fully-functional sidebar.
The simplest reason I can give is that most sidebars I've come across don't reach the absolute minimum threshold for UI/UX that a sidebar should have. Keep in mind, these are all completely subjective (and perhaps overly pedantic to a certain group of people.)
First, let's outline a few guiding principles.
Nevertheless, the most satisfying UI/UX experiences I've had when using sidebars have been as a result of a few core principles:
This might be a matter of preference, but once a user clicks on a sidebar item, it should render a new route. That is, as opposed to using a state management system like Redux to display different components depending on what the user selected.
The best part about implementing a sidebar this way is that users aren't broken out of the normal flow they are probably used to when using a browser. For instance, they can take advantage of built-in browser functionality like the back & forward buttons.
Here is an example of a sidebar that isn't linked to the browser's navigation history:
Being linked to the navigation alone isn't enough. A proper sidebar should also 'remember' its navigation state once a user refreshes the page.
Here is what it looks like when the sidebar doesn't remember the current state, i.e. when the browser refreshes, the sidebar goes back to the original page.
It's a major pet peeve of mine and perhaps the main issue I've had with most sidebar react libraries.
The same issue can also be seen when you navigate to other routes using the 'back' or 'forward' buttons.
In this example, despite the route changing when the 'back' and 'forward' buttons are used, they don't affect the currently selected item on the sidebar.
Incidentally, this is somewhat difficult to get dead right, and probably the whole reason I wrote this blog post. I struggled a bit before I was able to implement it in a way I thought was satisfying.
It has to be sticky.
Overflowing sidebars are just bad for user experience. With an overflowing sidebar, if you're all the way at the bottom of the page and need to redirect to a different route, you have to scroll back to the top first.
This is also surprisingly hard to achieve, but I'll show you how with some CSS trickery.
Here's an example of an overflowy sidebar:
A lot of other other libraries (eg. react-burger-menu have a ton of features I'll obviously be too lazy to add (animations, etc). Make no mistake, our project is going to be awesome, too, but don't expect too much flashiness (Just a not-too-much-but-still-kinda-lots amount because I love to get carried away with these tutorials :D).
PS:
We're going to need the following dependencies:
You can install them by running
npm install react-router-dom styled-components --save
// or
yarn add react-router-dom styled-components
That is, of course, assuming you already have a project up and running. If not, it's as easy as
npx create-react-app react-sidebar-project
Then
npm install react-router-dom styled-components --save
With that, let's get started!
First, we're going to set up four different dummy pages so we can route through all of them from a single component.
Each of them looks something like this:
import React from 'react';
function Page1(props) {
return (
<div>
<h1>Page 1</h1>
<p>I bring the sauce.</p>
</div>
);
}
export default Page1;
Up to page 3.
The fourth page I didn't mention is just a 404 page (because ¯_(ツ)_/¯)
It looks like this:
import React from 'react';
const NotFound = props => {
return (
<div>
<p>You took a wrong turn, mate.</p>
</div>
);
};
export default NotFound;
The other three files I didn't mention are:
Nav.jsx
which is relatively unimportant.Sidebar.jsx
where 3/4 of the magic happens.Layout.jsx
which is basically the parent component.Here's the folder structure:
.
├── package.json
├── public // You can safely ignore these
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── components
│ │ ├── Layout.jsx // This is the 'parent' component. It contains both Nav, Sidebar and imports Routes
│ │ ├── Nav.jsx
│ │ ├── Sidebar.jsx
│ │ └── SidebarItems.jsx
│ ├── index.css
│ ├── index.js // React is initialized through this file
│ ├── pages
│ │ ├── Dashboard.jsx
│ │ ├── NotFound.jsx
│ │ ├── Page-1.jsx
│ │ ├── Page-2.jsx
│ │ └── Page-3.jsx
│ └── routes.js // we define our routes here
└── yarn.lock
A central theme of all dashboards is that the sidebar should remain the same throughout the whole application. That means that any time the route changes, the sidebar shouldn't be affected.
Here's how to do it with react-router.
import React from "react";
import {BrowserRouter, Route, Switch} from "react-router-dom";
import Page1 from "./pages/Page-1";
import Page2 from "./pages/Page-2";
import Page3 from "./pages/Page-3";
import Dashboard from "./pages/Dashboard";
import NotFound from "./pages/NotFound";
function Routes() {
return (
<BrowserRouter>
<Switch>
<Route path="/" exact component={Dashboard}/>
<Route path="/page-1" component={Page1}/>
<Route path="/page-2" component={Page2}/>
<Route path="/page-3" component={Page3}/>
<Route component={NotFound}/>
</Switch>
</BrowserRouter>
)
}
export default Routes;
If you're familiar with react-router, there's nothing too foreign there.
Let's move on to Layout.jsx
:
import React from 'react';
import Routes from "../routes";
import Sidebar from "./Sidebar";
function Layout(props) {
return (
<div>
<p>I'm the daddy</p>
<div>
<Sidebar/>
<Routes/>
</div>
</div>
);
}
export default Layout;
Don't worry about the sidebar for now - it's just an empty shell.
Here's what we have so far:
Since I'm not very good with colors, I rely on this handy tool whenever I need a set of colors that always looks good. Let's go with a deep shade of red (#c34a36) that's too overlooked.
Next, we need a foundation for what we want the sidebar to look like:
It should be at least 250px wide.
It should always fit the screen height.
After a bit of CSS:
import React from 'react';
import Routes from "../routes";
import Sidebar from "./Sidebar";
import Nav from "./Nav";
function Layout(props) {
return (
<div>
<div style={{display: "flex"}}>
<Sidebar/>
<div>
<Nav/>
<Routes/>
</div>
</div>
</div>
);
}
export default Layout;
import React from "react";
import styled from 'styled-components'
const SidebarParent = styled.div`
background: #c34a36;
width: 250px;
height: 100vh;
`;
function Sidebar() {
return (
<>
<SidebarParent>
<h2>I'm the sidebar.</h2>
<p>Love me. Or else.</p>
</SidebarParent>
</>
);
}
export default Sidebar;
The boring and basic way to do this is simply including all the sidebar routes inside your Javascript file:
function Sidebar() {
return (
<>
<SidebarParent>
<SidebarItem>
<p>Dashboard</p>
</SidebarItem>
<SidebarItem>
<p>Page 1</p>
</SidebarItem>
<SidebarItem>
<p>Page 2</p>
</SidebarItem>
<SidebarItem>
<p>Page 3</p>
</SidebarItem>
</SidebarParent>
</>
);
}
But I am lazy and I'll assume you're a decent programmer. Being a decent programmer, one of the things youll want to achieve is DRY code. It's a clever acronym that stands for Don't Repeat Yourself. I won't get into too many details, but DRY code is easier to maintain and enables reusability.
Hard-coding all the items is fine if you don't plan on using the code in other parts of your application, but creating an abstraction on top of repetitive code is good practice. So, let's do just that.
In a new file:
const SidebarItems = [
{
name: "Dashboard"
},
{
name: "Page 1"
},
{
name: "Page 2"
},
{
name: "Page 3"
},
];
export default SidebarItems;
And our sidebar changes to:
function Sidebar() {
return (
<>
<SidebarParent>
{
SidebarItems.map(item=> (
<SidebarItem key={item.name}>
<p>{item.name}</p>
</SidebarItem>
))
}
</SidebarParent>
</>
);
}
Using styled-components, we can churn out the following sidebar
const SidebarParent = styled.div`
background: #cf3d2a;
width: 250px;
height: 100vh;
`;
const SidebarItem = styled.div`
padding: 16px 24px;
transition: all 0.25s ease-in-out;
//Change the background color if 'active' prop is received
background: ${props => props.active ? "#b15b00" : ""};
margin: 4px 12px;
border-radius: 4px;
p {
color: white;
font-weight: bold;
text-decoration: none;
}
&:hover {
cursor:pointer;
}
&:hover:not(:first-child) {
background: #c34a36;
}
`;
And the actual logic:
function Sidebar({defaultActive}) {
//If no active prop is passed, use `1` instead
const [activeIndex, setActiveIndex] = useState(defaultActive || 1);
return (
<>
<SidebarParent>
{
SidebarItems.map((item, index)=> {
return (
<SidebarItem key={item.name} active={index === activeIndex}>
<p>{item.name}</p>
</SidebarItem>
);
})
}
</SidebarParent>
</>
);
}
And with that, the easy part if over! Now we have something respectable:
Abstracting the sidebar in this way is useful because if you ever want to, say, change the background color of the sidebar item, you only have to do it in a single place. In addition, think about how much easier it will be to use an abstracted component if the sidebar items are dynamic and need to be loaded from the server.
Next up, we need to get to the core functionality of our app: redirect users to different routes depending on what's been clicked.
Since we're using react-router
, it might be tempting to just call useHistory
and get it over with, but it's important to keep in mind how our document
is structured.
Look at the graph below:
Notice how only Dashboard
and Page 1
receive the location props.
Indeed, if we tried to log the incoming props, you'll notice the
Layout
and Sidebar
props are empty.
This puts us in a pickle because we need access to those props inside the sidebar!
Let's take a step back and rewrite our route component so the prop is somehow passed down.
function Routes() {
return (
<BrowserRouter>
<Route render={(props)=>(
//Layout and sidebar can now receive props
<Layout {...props}>
<Switch>
<Route path="/" exact component={Dashboard}/>
<Route path="/page-1" component={Page1}/>
<Route path="/page-2" component={Page2}/>
<Route path="/page-3" component={Page3}/>
<Route component={NotFound}/>
</Switch>
</Layout>
)}/>
</BrowserRouter>
)
}
Next, our sidebar needs to know where each item should redirect the user to
const SidebarItems = [
{
name: "THE ITALIAN JOB",
route: '/'
},
{
name: "Dashboard",
route: '/dashboard',
},
{
name: "Page 1",
route: '/page-1'
},
{
name: "Page 2",
route: '/page-2'
},
{
name: "Page 3",
route: 'page-3'
},
];
export default SidebarItems;
After making a minor change to our Sidebar.js
file:
function Sidebar(props, {defaultActive}) {
const [activeIndex, ] = useState(defaultActive || 1);
return (
<>
<SidebarParent>
{
SidebarItems.map((item, index)=> {
return (
<Link to={item.route}> //redirect users to a new route
<SidebarItem key={item.name} active={index === activeIndex}>
<p>{item.name}</p>
</SidebarItem>
</Link>
);
})
}
</SidebarParent>
</>
);
}
Our app is navigation-ready!
The first part of our not-so-complex app is done. Let's move on to a slightly more complicated problem.
Here is the current problem:
Every time the page is refreshed, the sidebar resets to 'index 0' making it highlight the first item on the list. We need to make it not do that.
We also need to synchronize the sidebar and the url so that heading to '/page-2' on the url bar also redirects us to the correct route on the sidebar.
Once this step is completed, our sidebar should look something like this:
Believe it or not, all the changes we need to make are going to be right
within the Sidebar.js
file. Magical, isn't it?
Here's what the code for our new sidebar looks like:
function Sidebar(props, {defaultActive}) {
//Those location props finally come in handy!
const location = props.history.location;
//Load this string from localStorage
const lastActiveIndexString = localStorage.getItem("lastActiveIndex");
//Parse it to a number
const lastActiveIndex = Number(lastActiveIndexString);
//Store it in state
const [activeIndex, setActiveIndex] = useState(lastActiveIndex || Number(defaultActive));
//This sets the item to localStorage and changes the state index
function changeActiveIndex(newIndex) {
localStorage.setItem("lastActiveIndex", newIndex)
setActiveIndex(newIndex)
}
//Appends '/' to the start of a string if not present
function getPath(path) {
if (path.charAt(0) !== "/") {
return "/" + path;
}
return path;
}
//This re-renders when the route changes
useEffect(()=> {
//Get an item with the same 'route' as the one provided by react router (the current route)
const activeItem = SidebarItems.findIndex(item=> getPath(item.route) === getPath(location.pathname))
changeActiveIndex(activeItem);
}, [location])
//...
}
(The above code represents my thought process, not necessarily the best way to go about it. Notice how localStorage isn't even necessary at this point! It was the first idea I had but ultimately ended up being useless. The final code won't include it)
I hope you've been having fun because it's now crunch time. We need to get rid of annoying overflowy text :/.
Here's what we don't want:
In other words, we want the sidebar to be fixed. If your instincts
tell you to slap on position:fixed
and move on, don't listen to it. Or
at least not exactly.
There are a couple of ways to fix this problem, so let's cover a few of them:
Using position:fixed
: The css properties position:fixed
and position:absolute
break the flow of the page.
Elements on the page will jump around if you use them as-is.
Using position:fixed
on the sidebar and margin-left:auto
on the
rest of the body will fix the sidebar in place and align the body to the right.
Note: margin-left:auto
works this way because of
peculiar flexbox magic.
It's a useful trick that will definitely come in handy.
However, this solution is horribly unresponsive, and implementing the same is 10x harder than other solutions. If you just want something quick and dirty, though, it should work fine.
Using position:sticky
: A better solution would be to use
position:sticky
and top:0
. This would make the sidebar 'sticky'.
A 'sticky' element, hmm..., sticks to a certain area of the screen while still behaving like a 'relative' element. In other words, it doesn't break the flow, and styling using flexbox or grid will work. I personally prefer this solution because responsiveness became 1000x easier.
The major issue with this is probably
browser compatibility and
a few quirky quirks
around using both sticky
and overflow
together . It doesn't always work and debugging it
can be a real pain in the butt.
If you take issue with either one of those solutions, I offer you a slightly more hacky and convoluted way to do it but always guaranteed to work.
Here's how to do it:
We need to modify the sidebar slightly.
First, create a SidebarParent
component using styled-components and give it a two
children - a 'skeleton' div
and another containing our actual
sidebar items. The skeleton div's usefulness will become apparent in a
moment.
<SidebarParent>
<div>
//Sidebar items
</div>
<div className="im-a-skeleton"/>
</SidebarParent>
This is what SidebarParent
looks like:
//SidebarParent.jsx
a {
text-decoration: none;
}
& > div {
width: 250px;
height: 100vh;
}
This gives every direct child of SidebarParent
a fixed width of '250px' (including
our skeleton div)
If we make our sidebar fixed
, it breaks the flow of the page. However, since the
skeleton is part of the same parent and its position is 'block' (by default),
it moves 'underneath' the now flow-broken sidebar. (Alternatively, you can think
of it as the Sidebar moving 'on top' of the skeleton.)
Without the skeleton, the rest of the dashboard would be what would occupy the space left behind by the sidebar. We don't need any extra styling to make the rest of the page play nice.
Do note that this also poses its own complications - the sidebar will slide on top of the content if the screen is small enough (fixed width, remember?) - but it's ultimately up to you to decide how to implement it.
Voila! Now we have a perfectly good react sidebar with routing functionality and 'state awareness',
Cheers!
Copyright © 2021 The Kenyan Dev