No Time Dad

A blog about web development written by a busy dad

Sliding Sidenav with React

Intro

There are sidenavs out there on the internet that don’t look like the standard sidenavs you usually see. They break the mold from Bootstrap, Material, or whatever other web framework most people are using these days. That isn’t to say that they’re better, just different.

I stumbled on one of those different sidenavs recently on the Canal Street Market site. The site has four main sections, each represented as a column on the right unless the section is open. When a column is clicked on it will transition over to the left or right and the content will fade into view.

canal_street_homepage

From a design perspective, I think this is fantastic. It’s a fresh take on navigation, and it incorporates some interesting css and JavaScript concepts to make it work. I really haven’t seen another page with navigation like this before.

From a usability perspective, I don’t think it’s the best. And this is just my personal opinion, but I think heavy usage of transitions can make sites appear much slower than they really are. I see the appeal of using them, but I like having fast websites and try to work hard to keep my sites fast. So, I’m not sure if I’d intentionally make my site appear slow. But, again, just my personal taste. With that being said, the Canal Street site itself is so far above my skillset it’s almost laughable.

So, I thought it would be fun to dig into this navigation and see if I can create a similar version. I’m not sure what framework is being used behind the scenes on the Canal Street Market site, but I’ll be using React for my version. You can view the demo and final code here.

Initial considerations

The component is made up of four panels. There will be one, and only one, open panel at a time. Clicking on a closed panel should open that panel and close the currently open panel. What all of this means is that I need to keep track of which panel is currently open. Anytime I need to “keep track” of something means that I’m adding state to the component.

Another thing to keep in mind is that I don’t want clicking on the content to close or open the panel. Only clicking on the columns should open or close a panel. The content should be independent from the open and close functions.

Lastly, what is the best layout? Should I use grid, flexbox, or something else? I ended up using flexbox because I wanted to be able to transition on the width property, but I think it could easily be written using grid too. I’m just not sure which property I’d transition on when using grid. I did, eventually, remove all of the transitions anyways but at that point I was already deep into flexbox usage and didn’t want to turn back.

Getting started

I approached this component by first just getting a basic structure built. No click handlers or dynamic classes. Just a basic outline of what the component will look like. I’ll hardcode the first panel as the open panel and leave the rest closed.

One
Two
Three
Four
import React from 'react';
import * as styles from './styles.module.css';

export const SlidingPanels = () => (
  <div className={styles.Wrapper}>
    <div className={`${styles.Panel} ${styles.Panel__Open}`}>One</div>
    <div className={styles.Panel}>Two</div>
    <div className={styles.Panel}>Three</div>
    <div className={styles.Panel}>Four</div>
  </div>
);
.Wrapper {
  /* Ensure that panels take up entire page */
  height: 100vh;
  /* Init flexbox to be used later */
  display: flex;
}

.Panel {
  /* Init flexbox to be used later */
  display: flex;
  flex-direction: column; 
}

/* Lazy way to set the background-color. A little brittle, so be careful. */
.Panel:nth-child(1) { background-color: lightcoral; }
.Panel:nth-child(2) { background-color: lightcyan; }
.Panel:nth-child(3) { background-color: lightgreen; }
.Panel:nth-child(4) { background-color: lightskyblue; }

.Panel__Open {
  /* The open panel should take all available space */
  width: 100%;
}

If the third panel were open, it would look like the below. Clicking on column One or Two should close the green section and push it back across. Flexbox takes care of most of the layout work for me.

One
Two
Three
Four
export const SlidingPanels = () => (
  <div className={styles.Wrapper}>
    <div className={styles.Panel}>One</div>
    <div className={styles.Panel}>Two</div>
    <div className={`${styles.Panel} ${styles.Panel__Open}`}>Three</div>
    <div className={styles.Panel}>Four</div>
  </div>
);

Handling clicks

Currently, the panels don’t do anything when clicked. That, obviously, needs to change. I need to add state to track the open panel, and click handlers to update which panel is open.

At this point, I can already tell that the panels portion of my SlidingPanels component is going to get complicated. I’m going to be using click handlers and changing styles dynamically, so this is probably a good time for me to create a separate Panel component.

The new Panel component will be stateless. It will receive a prop from its parent component that tells it whether is should be open or not. It will also recieve a prop from the parent component that allows it to change itself to be the open panel when it’s clicked on. For this, I’ll use React’s useState hook. As mentioned above, I’ll pass the state value and the function to set the state value down to each Panel component.

If a Panel component is the active panel, I’ll add the Panel__Open selector to it and remove the selector from the previously open panel. This will happen via a ternary on the className string.

I’ll update my jsx file as shown below. The css will remain the same for now. Try clicking on the panels in the component shown below to see how it behaves.

import React, { useState } from 'react';

export const SlidingPanels = () => {
  const [activePanel, setActivePanel] = useState(1);
  return (
    <div className={styles.Wrapper}>
      <HandleClicksPanel
        activePanel={activePanel} 
        setActivePanel={setActivePanel}
        panelId={1}
        panelDisplayName="One" />

      <HandleClicksPanel
        activePanel={activePanel}
        setActivePanel={setActivePanel}
        panelId={2}
        panelDisplayName="Two" />

      {/* Other panels omitted for space */}
    </div>
  )
}

const Panel = ({ activePanel, setActivePanel, panelId, panelDisplayName }) => (
  <div
    onClick={() => setActivePanel(panelId)}
    className={`${styles.Panel} ${activePanel === panelId ? styles.Panel__Open : ''}`}>
    {panelDisplayName}
  </div>
);
One
Two
Three
Four

Improving the panels

I’m heading in the right direction now, but there are a few problems. The first problem is that this component is not accessible. The closed panels should be link elements instead of div elements so they can be selected with or without a mouse. The second problem is that once I switch the panels to be anchor links the entire panel will clickable and I only want the closed panels to be clickable.

There are probably a few different ways to fix these problems, but I decided that the best way for me was to add a new child element in the Panel component to represent the clickable link tab. The tab will only be shown when a given panel is closed. I’ll move the click handler to the new child element instead.

Another improvement I can make here is add the ability to pass content to the panel. I eventually want to be able to render something in the open panel. A convenient way in React to make content rendering more dynamic is to use props.children. So, anything I put between <Panel></Panel> will be shown on the open panel. I can then map other components to the open panels later. For this example, I’ll create four small content components and pass them as children to the panel component to be shown when the panel is open.

Lastly, I’ll make some style changes that include adding spacing to the content, and adding spacing and rotation to the closed panel text. The final demo and code are shown below.

Demo and final code

Content 1
import React, { useState } from 'react';
import * as styles from './styles.module.css';

export const SlidingPanels = () => {
  const [activePanel, setActivePanel] = useState(1);
  return (
    <div className={styles.Wrapper}>
      <Panel
        activePanel={activePanel}
        setActivePanel={setActivePanel}
        panelId={1}
        panelDisplayName="One">
        {activePanel === 1 && <Content1 />}
      </Panel>

      <Panel
        activePanel={activePanel}
        setActivePanel={setActivePanel}
        panelId={2}
        panelDisplayName="Two">
        {activePanel === 2 && <Content2 />}
      </Panel>

      <Panel
        activePanel={activePanel}
        setActivePanel={setActivePanel}
        panelId={3}
        panelDisplayName="Three">
        {activePanel === 3 && <Content3 />}
      </Panel>

      <Panel
        activePanel={activePanel}
        setActivePanel={setActivePanel}
        panelId={4}
        panelDisplayName="Four">
        {activePanel === 4 && <Content4 />}
      </Panel>

    </div>
  );
}

const Panel = ({ activePanel, setActivePanel, panelId, panelDisplayName, children }) => (
  <div className={`${styles.Panel} ${activePanel === panelId ? styles.Panel__Open : ''}`}>
    {activePanel !== panelId &&
      <a
        onClick={() => setActivePanel(panelId)}
        href={`#${panelId}`}
        className={styles.Panel__Tab}>
        <div className={styles.Panel__Text}>
          {panelDisplayName}
        </div>
      </a>}
    {children}
  </div>
);

const Content1 = () => <div className={styles.Content}>Content 1</div>;
const Content2 = () => <div className={styles.Content}>Content 2</div>;
const Content3 = () => <div className={styles.Content}>Content 3</div>;
const Content4 = () => <div className={styles.Content}>Content 4</div>;
.Wrapper {
  height: 100vh;
  display: flex;
}

.Panel {
  display: flex;
  flex-direction: column; 
}

.Panel:nth-child(1) {
  background-color: lightcoral;
}
.Panel:nth-child(2) {
  background-color: lightcyan;
}
.Panel:nth-child(3) {
  background-color: lightgreen;
}
.Panel:nth-child(4) {
  background-color: lightskyblue;
}

.Panel__Open {
  width: 100%;
}

.Panel__Tab {
  flex: 1;
  padding: 1rem;
  color: black;
  text-decoration: none;
}

.Content {
  margin: 2rem;
}

.Panel__Text {
  margin-top: 1rem;
  transform: rotate(90deg);
}

Conclusion

This component turned out better than I expected, but it’s not without its issues. The main issue is that it doesn’t use any type of router, so when you click the panel the content isn’t mapped to a route. This is problematic for a few reasons, the main one being SEO. I think it’s a better user experience to change the URL instead of appending an anchor.

This component required a lot less css than I was expecting. Of course, my version is not nearly as fancy as the Canal Street Market version, but it’s a good starting point to build on. A confidence builder.