Gatsby - Part II: Sidebar

August 29, 2019·11 min read
  • Web Development
  • Gatsby
  • React
  • JavaScript
  • CSS

This is the second post in the series about creating a website with Gatsby, in which I’ll be examining a technique for building a sidebar.

As I’ve mentioned earlier in my first post, I wanted my blog to have a strong visual signature. I figured one way to achieve this is by having a permanent component across all pages with a visual item unique to me; say, my profile image. Moreover, it is probably a good idea to give quick access to all of the site’s sections from any given page. It also happened that one of the Gatsby starter sites — Lumen v2, the one I eventually used — featured what seemed to me as a nice looking sidebar. That how I knew I wanted to have a permanent sidebar in my blog.

After playing around with several layouts for the sidebar, I’ve decided to go with these components:

  • A profile image.
  • A title & a subtitle.
  • Contact links in the form of icons.
  • A theme toggle button.
  • A menu to navigate the different sections of the website.

Creating a design for a sidebar with all of these components becomes a bit of a problem when considering mobile screens. Mobile screens are just not wide enough to contain both the content of a page and the components of the sidebar next to it. The solution, as some of you mobile readers have probably noticed, is to make the sidebar a “topbar” on smaller screens, hovering at the top of the screen. This is hardly a new concept, and some might say it is a standard in mobile design.

That wasn’t enough though, as one “line” at the top of a narrow screen wasn’t enough to contain all of the components. Another modification was due then, and this time I chose to make both of the lists in the bar — the contact links and the navigation menu — collapsible; i.e, hidden at first but able to come into view with the click of a button.

Collapsing Modes

One approach to collapsible components is to think of each “collapsing state” individually. In the case of my sidebar, there are 3 states - the initial “main” state with all the lists hidden, the state in which the navigation menu is shown and the contact links are hidden, and lastly the opposite state where the contact links are shown and the navigation menu is hidden:

Sidebar/index.jsxjsx
import React from 'react'

const SidebarMode = Object.freeze({
  Main: "main",
  Menu: "menu",
  Contact: "contact"
})

class Sidebar extends React.Component {
  constructor(props) {
    super(props)
    this.state = { mode: SidebarMode.Main }
  }
}
export default Sidebar

Since we’re coding with React, which already has a different use for the term “state”, I’ll be referring to these collapsing states as “modes” from now on.

In each mode, some components are shown and some are hidden. Well, how do you hide components? You can do that by changing the display or visibility CSS properties of the components, but sometimes you want to have a more elaborate design; hence, it is best to assign a custom CSS class which indicates whether a component is enabled or disabled, and control each component behavior individually via CSS.

Let’s create a generic component to control the CSS class determining whether our individual components are enabled or disabled:

Toggle/index.jsxjsx
import React from 'react'

class Toggle extends React.Component {
  render() {
    return (
      <div className={(this.props.isEnabled) ? "enabled" : "disabled"}>
        {this.props.children}
      </div>
    )
  }
}
export default Toggle

Let’s also accompany it with some SASS mixins:

mixins/toggle.scssscss
@mixin enabled {
  .enabled > & {
    @content;
  }
}
@mixin disabled {
  .disabled > & {
    @content;
  }
}

And we will be using the Toggle component as so:

jsx
const title = (
  <Toggle isEnabled={this.state.isTitleEnabled}>
    <h1 className="title">Eyal Roth</h1>
  </Toggle>
)
scss
@import "mixins/toggle";

title {
  @include enabled {
    transition: opacity 0.2s ease-in;
    opacity: 1;
  }
  @include disabled {
    opacity: 0;
  }
}

Changing the CSS class of a component, or any other attribute for that matter, is impossible once the component has been created. Sure, it is possible to modify the DOM after the render method in either componentDidMount or componentDidUpdate, but that is rather cumbersome and prone to error. This is why the Toggle component doesn’t add the CSS class to the individual components’ class attribute, but rather wraps them with a div element annotated with said class.

The SASS mixins make sure to apply the styling only to direct descendants (>) of the toggle divs, making it possible to nest Toggle components without them interfering with one another.

Back to the sidebar components, each of them is either enabled or disabled according to the sidebar mode. Let’s create a generic component to toggle our sidebar components according to each mode:

Sidebar/index.jsxjsx
import Toggle from '../Toggle'

class SidebarToggle extends React.Component {
  render() {
    return (
      <Toggle isEnabled={this.isEnabled()}>
        {this.props.children}
      </Toggle>
    )
  }
  isEnabled() {
    // eslint-disable-next-line
    switch (this.props.mode) {
      case SidebarMode.Main:
        return this.props.main
      case SidebarMode.Menu:
        return this.props.menu
      case SidebarMode.Contact:
        return this.props.contact
    }
  }
}

And now we can render all kind of different components in the sidebar:

Sidebar/index.jsxjsx
  renderTitle() {
    return (
      <SidebarToggle main={true} menu={false} contact={false} {...this.state}>
        <h1 className="sidebar__title">Eyal Roth</h1>
      </SidebarToggle>
    )
  }

  renderMenu() {
    return (
      <SidebarToggle main={false} menu={true} contact={false} {...this.state}>
        <nav className="sidebar__menu">
          {/* ... */}
        </nav>
      </SidebarToggle>
    )
  }

The title will be enabled for the Main mode, and disabled for the other two modes, while the menu will be enabled only for the Menu mode. The mode itself is passed from the Sidebar’s own state to the SidebarToggle. To make the menu collapsible, we need to make use of some (S)CSS:

Sidebar/style.scssscss
@import "mixins/toggle";

$z-index: 1;

sidebar {
  z-index: $z-index; // above the page content
  &__menu {
    position: absolute;
    width: 80vw;
    right: 0; // if you want to make it appear from the right
    transition:
        width 0.2s ease-out,
        opacity 0.2s ease-in,
    ; 
    @include enabled {
        opacity: 1;
        z-index: $z-index; // same as the sidebar itself
    }
    @include disabled {
        opacity: 0;
        width: 0;
    }
  }
}

Also, let’s not forget the buttons that will allow the user to toggle between the different modes:

Sidebar/index.jsxjsx
  renderMenuButton() {
    const isEnabled = this.state.mode === SidebarMode.Menu
    const newMode = (isEnabled) ? SidebarMode.Main : SidebarMode.Menu
    return (
      <Toggle isEnabled={isEnabled}>
        <button className="sidebar__menu-button" onClick={() => this.changeMode(newMode)}>
          <i title="Menu" className="icon-menu" />
        </button>
      </Toggle>
    )
  }

  changeMode(newMode) {
    this.setState({ mode: newMode })
  }

Remember that the buttons might also have different styling when they are either enabled or disabled. This time we’re using the simple Toggle instead of SidebarToggle, since we need to make use of the isEnabled variable to also determine what would be the mode that a button click would switch to.

And this is how our sidebar component render method would look like:

Sidebar/index.jsxjsx
  render() {
    return (
      <div className="sidebar">
        {this.renderMenuButton()}
        {this.renderProfileImage()}
        {this.renderTitle()}
        {this.renderMenu()}
        {this.renderContact()}
        {this.renderThemeButton()}
        {this.renderContactButton()}
      </div>
    )
  }

The ordering of the components should mostly reflect their order on larger screens, since with the smaller screens — where the sidebar is a topbar — we’re controlling the components with absolute CSS positions.

Speaking of larger screens, we need to make sure that all of the work we’ve done for mobile screens won’t reflect in the design of the bar-on-the-side. A common approach to designing a website is the “mobile first” approach: By default, you style the website for smaller screens, and then you add modifications to adjust the design for larger screens:

Sidebar/style.scssscss
@media screen and (min-width: 900px) { // this should be encapsulated in a mixin
  sidebar {
    z-index: unset;
    &__menu {
      position: unset;
      @include enabled {
          z-index: unset;
      }
      @include disabled {
          opacity: unset;
          width: unset;
      }
    }
    &-button {
      display: none;
    }
  }
}

Here, we are undoing all of the styling we previously declared which will interfere with the larger screens design. It might seem as we only need to worry about the Main state, since the concept of “modes” doesn’t exist for larger screens (and that’s why we’re completely removing the buttons from the display), but in fact it is. First, devices can switch between portrait and landscape orientations, drastically changing the screen size. Then there’s the option to reduce the window size on desktop screens, especially the width of the window. Lastly, this really helps when debugging your website in the developer tools, constantly switching between mobile and desktop views.

Positioning

Up until now we’ve been dealing with the lack of space on mobile screens via collapsing components and multiple modes, but we have yet to position the sidebar on either small or large screens. In order to control the positioning of the sidebar, we first have to consider the general layout of a page in the site. For the purpose of this post we’ll be using a simple page layout. I will be examining a more elaborate layout on a later post in this series.

Layout/index.jsxjsx
import React from 'react'
import Sidebar from '../Sidebar'
import './style.scss'

class Layout extends React.Component {
  render() {
    return (
      <div className="page-container">
        <Sidebar/>
        <div className="content">
          {this.props.children}
        </div>
      </div>
    )
  }
}

Every page in the website will render the Layout component as its top-most component, which will enforce the following styling:

Layout/style.scssscss
$topbar-height: 50px;
content {
  width: 90vw;
  margin: 0 auto;
  padding-top: $topbar-height;
}
sidebar { // actually located at Sidebar/style.scss
  position: fixed;
  width: 100vw;
  top: 0;
  height: $topbar-height;
}

@include breakpoint-md { // the @media query encapsulated in a mixin
  $content-width-md: 600px;
  content {
    width: $content-width-md;
    padding-top: 0;
  }
  sidebar {
    width: 230px;
    margin-left: calc(50vw + #{$content-width-md} / 2);
    height: 70vh;
    padding-top: 30vh;
  }
}

This is the most bare-bone styling needed to make the sidebar appear at the top of the page on smaller screens and to the side of the page on larger screens. There are actually multiple ways of achieving this behavior, as CSS is rather flexible, but I’ve found this technique to be simple and intuitive. Note that on smaller screens, the content is “shifted” down just enough so the topbar will not cover the top of the content.

One important thing to note here is that this styling will behave the same for all screens larger than the “md” breakpoint (900px), and it’s rather hard to find the exact styling that would fit this large variety of screens. Instead, you should add more breakpoints to adjust the styling according to sub-set of screen sizes. For my website, I’ve been using these breakpoints in my stylesheets:

  • xsm: 360px; - handsets in portrait mode.
  • sm: 600px; - handsets in landscape mode, tablets in portrait mode.
  • md: 900px; - large handsets (landscape), tables (landscape), large tablets (portrait).
  • lg: 1280px; - desktop.

Remember, it’s not just about different devices, but it’s also relevant for desktop screens when considering the ability to resize the window.

Peeking Topbar

For the last part of this post, we’ll slightly improve the behavior of the topbar by making it disappear when scrolling down a page, and reappearing on scroll up; hence, a “peeking topbar”. Once again we will be using the toggle technique to style the appearance and disappearance of the topbar:

Sidebar/style.scssscss
sidebar {
  transition: transform 0.5s ease-out;
  @include disabled {
    transform: translateY(-$topbar-height - 10);
  }
}

@include breakpoint-md 
  sidebar {
    @include disabled {
      transform: unset;
    }
  }
}

We only need to define the disabled mode since the sidebar is enabled by default. Note that we’re using transform: translateY() instead of top. The reason for this is because we want to disable this disappearance behavior on larger screens — even when scrolling down a page — but we don’t want to accidentally disable any other top styling that we might choose to set for larger screens.

Now that we have the styling in mind, we’ll need to determine when the sidebar is enabled or not. Since the object-oriented paradigm is nice and composition over inheritance is a good principle, we’ll encapsulate the “peeking” functionality in a new component:

Sidebar/index.jsxjsx
class Sidebar extends React.Component {
  render() {
    return (
      <PeekingToggle>
        <div className="sidebar">
          {/* same as before */}
        </div>
      </PeekingToggle>
    )
  }
}

class PeekingToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {isEnabled: true}
    this.lastScrollY = null
    this.scrollUpRate = {
      firstEventTime: null,
      lastEventTime: null,
      firstScrollY: null
    }

    // we need to bind `this` because these will be invoked as an event listener
    this.handleScroll = this.handleScroll.bind(this)
    this.isFastScrollUp = this.isFastScrollUp.bind(this)
  }

  render() {
    return (
      <Toggle isEnabled={this.state.isEnabled}>
        {this.props.children}
      </Toggle>
    )
  }

  componentDidMount() {
    window.addEventListener('scroll', this.handleScroll)
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll)
  }

  handleScroll(event) {
    const scrollY = window.scrollY
    
    const isScrollUp = this.lastScrollY && this.lastScrollY > scrollY
    this.lastScrollY = scrollY
    const isFastScrollUp = this.isFastScrollUp(event.timeStamp, scrollY, isScrollUp)

    const isInTopOfPage = scrollY <= window.innerHeight
    const isEnabled = isInTopOfPage || isFastScrollUp || (this.state.isEnabled && isScrollUp)

    if (isEnabled !== this.state.isEnabled) {
      this.setState({isEnabled})
    }
  }

  isFastScrollUp(eventTime, scrollY, isScrollUp) {
    const maxEventTimeGap = 100

    if (isScrollUp) {
      if (this.scrollUpRate.firstScrollY && 
          maxEventTimeGap > eventTime - this.scrollUpRate.lastEventTime) {
        this.scrollUpRate.lastEventTime = eventTime
      } else {
        this.scrollUpRate = {
          firstEventTime: eventTime,
          lastEventTime: eventTime,
          firstScrollY: scrollY
        }
      }

      const pixels = this.scrollUpRate.firstScrollY - scrollY
      const millis = this.scrollUpRate.lastEventTime - this.scrollUpRate.firstEventTime

      return pixels > 50 && (millis / pixels) < 10
    } else {
      this.scrollUpRate = {
        firstEventTime: null,
        lastEventTime: null,
        firstScrollY: null
      }
      return false
    }
  }
}

This is all just some “fancy” code to make sure the topbar is disabled when the page has been scrolled down at least one “screen height” (100vh), and that it reappears only if it’s scrolled up “fast enough” (go ahead and adjust the numbers to your liking).

That’s it for the sidebar. Stay tuned for the next posts in the series!