Creating responsive navbars with background images in GatsbyJS using gatsby-image

fidelve

FidelVe

Posted on October 3, 2019

Creating responsive navbars with background images in GatsbyJS using gatsby-image

gatsby logo

Recently I decided to redo my personal static page with React, while searching the web for a way to generate statics React sites I found out about Gatsbyjs, and after a couple of hours of reading about it, my two days journey of learning the hard way started.

homepage horizontal

The main challenge I didn't know I was going to face, was porting the responsive navbar that I created for my site with simple CSS (flexbox, media queries, and background images).

The position of the navbar in my site changes from vertical to horizontal depending on the screen size of the browser, and, depending on this position, the background image changes.

homepage vertical

While reading the Gatsby official docs and tutorial, I found out about gatsby-image and decided to use it.

gatsby-image is a React component specially designed to work seamlessly with Gatsby’s GraphQL queries. It combines Gatsby’s native image processing capabilities with advanced image loading techniques to easily and completely optimize image loading for your sites.

By using gatsby-image you can optimize image loading, it automatically creates different images to use depending on display size and also applies progressive image loading (blur up), the problem for me was, that it doesn’t work with background images.

There is another package called gatsby-background-image, but, since I already had an idea on how to make it work, I decided not to use it.

The way I decided to implement it was simple, first create an absolute positioned container to display the image and then a second absolute positioned container for the links stacked on top.

<nav>
  <div>
  <!-- gatsby-image here -->
  </div>
  <div>
  <!-- navbar links here -->
  </div>
</nav>
Enter fullscreen mode Exit fullscreen mode

Creating the Project Structure

I am going to assume that you already have Gatsby installed and that you already created a project using the command gatsby new <project-name>.

In my case, the folder structure of my project looks like this (not displaying the node_modules and public folders).

project structure

Inside the src/components folder you can see 3 files:

  • layout.css a vary basic CSS reset with the following content:
html {
  font-family: sans-serif;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
body {
  margin: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
Enter fullscreen mode Exit fullscreen mode
  • layout.module.css. A CSS Module to avoid name collision. Here we are going to write all of our CSS code for our Layout Component.

  • layout.js our Layout Component.

Inside the src/images we have our two background images for when the navbar is either horizontal or vertically positioned, and inside the src/pages we have the 3 Page Components that are going to be linked at in the navbar. This is the code inside the Page Components.

import React from "react"

import Layout from "../components/layout"

const IndexPage = () => (
  <Layout headerText="Index Page Header text.">
    <p>
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua.
    </p>
  </Layout>
)

export default IndexPage

Enter fullscreen mode Exit fullscreen mode

Creating our layout Component

Because all the pages in my site are structured the same way, I decided to create a Layout Component, and use it across all the pages.

This Layout Component will have the responsive navbar, and the graphql queries.

import React from "react"
import { Link } from "gatsby"
import styles from "./layout.module.css"
import "./layout.css"

class Layout extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <div id={styles.layout}>
        <nav id={styles.navbar}>
          <div id={styles.navbarBackground}>
            {/* Background Image goes here */}
          </div>
          <div id={styles.navbarLinkContainer}>
            <Link className={styles.navLink} to="/">HOME</Link>
            <Link className={styles.navLink} to="/about/">ABOUT</Link>
            <Link className={styles.navLink} to="/contact/">CONTACT</Link>
          </div>
        </nav>
        <main id={styles.main}>
          <header>
            <h1>{this.props.headerText}</h1>
          </header>
          <div>{this.props.children}</div>
        </main>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Using CSS media queries

In order to change the position of the navbar we need to use CSS media queries. For my page, I decided to use a screen width of 580 pixels as the breakpoint in the query.

#layout {
  min-height: 100%;
  min-width: 320px;
}
#navbar,
#navbar-background,
#navbar-link-container {
  height: 60px;
}
#navbar {
  position: fixed;
  z-index: 999;
  top: 0px;
  width: 100%;
  background-color: #eee;
}
#navbar-background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  overflow: hidden;
}
#navbar-link-container {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  flex-flow: row nowrap;
  text-align: center;
}
.nav-link {
  font-size: 20px;
  display: block;
  background-color: rgba(0, 0, 0, 0.1);
  text-decoration: none;
  color: #333;
  font-weight: bold;
  padding: 20px 4px 0px 2px;
}
.nav-link:hover {
  background-color: rgba(0, 0, 0, 0.4);
  color: #fff;
}
#main {
  margin-top: 75px;
}

@media screen and (min-width: 580px) {
  /* if width more than 580px */
  #layout {
    display: flex;
    flex-flow: row nowrap;
    height: 100vh;
  }
  #navbar,
  #navbar-background,
  #navbar-link-container {
    width: 170px;
    height: 100vh;
  }
  #navbar {
    flex-grow: 0;
    flex-shrink: 0;
    position: static;
    background-color: rgba(0, 0, 0, 0.1);
  }
  #navbar-link-container {
    flex-flow: column nowrap;
  }
  #main {
    margin-top: 0px;
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 100px;
  }
}
Enter fullscreen mode Exit fullscreen mode

Detecting the navbar position inside React

Because we are using CSS queries to change the position of our navbar, and the image we are going to be displaying as a background changes depending on this position, we need to find a way to detect the navbar position from inside React and display the correct image.

This is the main reason why I implemented the Layout Component as a class component and not a function component. We need to have the sidebar position as a state inside the Layout Component and re-render it when it changes.

The way we are going to do this is by first, detect the position of the navbar on the first rendering, listening for resize events and detect when our navbar changes position based on our breakpoint (width < 580px).

//.....//
class Layout extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      navbarPosition: this.getPosition(),
    }
  }

  getPosition = () => {
    // This conditional is to avoid errors during build, check
    // https://www.gatsbyjs.org/docs/debugging-html-builds/
    if (typeof window !== "undefined") {
      return window.innerWidth < 580 ? "horizontal" : "vertical"
    }
  }
  componentDidMount() {
    window.addEventListener("resize", this.updateSize)
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.updateSize)
  }
  updateSize = () => {
    let position = this.getPosition()
    if (position !== this.state.navbarPosition) {
      // Changing the state will cause React to re-render, in
      // this case we are only changing the state when the
      // navbar changes position
      this.setState({ navbarPosition: position })
    }
  }
  render() {
    //.....//
  }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the basic structure, we need to use gatsby-image with graphql to display the images in our #navbar-background container.

Using graphql with gatsby-image

In order to use gatsby-image we need to query for the image files with graphql, if you don't know how graphql queries work on gatsby, I highly recommend to first read about it in here.

In my case the graphql query looks like this:

{
  navbarBgVer: file(relativePath: { eq: "navbar-ver.jpeg" }
  {
  ¦ childImageSharp {
  ¦ ¦ fluid(maxHeight: 1200) {
  ¦ ¦ ¦ ...GatsbyImageSharpFluid
  ¦ ¦ }
  ¦ }
  }
  navbarBgHor: file(relativePath: { eq: "navbar-hor.jpeg" }
  {
  ¦ childImageSharp {
  ¦ ¦ fluid(maxWidth: 2000) {
  ¦ ¦ ¦ ...GatsbyImageSharpFluid
  ¦ ¦ }
  ¦ }
  }
}

Enter fullscreen mode Exit fullscreen mode

There are 3 ways of using graphql with Gatsby, and which one to use, depends on several factors:

Because we are going to be making the graphql query outside a Page Component, we have to either use StaticQuery or useStaticQuery. For this case I decided to go with StaticQuery because is what am most comfortable with right now, useStaticQuery is a React Hook and even though we should be using them (they seem to be the way most developers will be using React from now on), I haven't yet learned them, so, rewriting my site to use React Hooks might be a good project for the future.

Adding the <StaticQuery /> with the graphql query to our layout.js component, our new render() function will look like this.

render() {
  const navbarPosition = this.state.navbarPosition
  return (
    <StaticQuery
      query={graphql`
        {
          navbarBgVer: file(relativePath: { eq: "navbar-ver.jpeg" }) {
            childImageSharp {
              fluid(maxHeight: 1200) {
                ...GatsbyImageSharpFluid
              }
            }
          }
          navbarBgHor: file(relativePath: { eq: "navbar-hor.jpeg" }) {
            childImageSharp {
              fluid(maxWidth: 2000) {
                ...GatsbyImageSharpFluid
              }
            }
          }
        }
      `}
      render={data => (
        <div id={styles.layout}>
          <nav id={styles.navbar}>
            <div id={styles.navbarBackground}>
              {/* gatsby-image components*/}
              {navbarPosition === "vertical" ? (
                <Img
                  style={{ height: "100%" }}
                  fluid={data.navbarBgVer.childImageSharp.fluid}
                />
              ) : (
                <Img
                  style={{ minHeight: "60px" }}
                  fluid={data.navbarBgHor.childImageSharp.fluid}
                />
              )}
            </div>
            <div id={styles.navbarLinkContainer}>
              <Link className={styles.navLink} to={"/"}>
                HOME
              </Link>
              <Link className={styles.navLink} to={"/about/"}>
                ABOUT
              </Link>
              <Link className={styles.navLink} to={"/contact/"}>
                CONTACT
              </Link>
            </div>
          </nav>
          <main id={styles.main}>
            <header id={styles.mainHeader}>
              <h1>{this.props.headerText}</h1>
            </header>
            <div id={styles.content}>{this.props.children}</div>
          </main>
        </div>
      )}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see we are rendering our entire Layout Component inside the <StaticQuery /> element acting as a wrapper. This <StaticQuery /> element takes two attributes

  • The first one is query. Inside of this attribute we are doing our graphql query.

  • The second one is render. This one takes a function and passes onto it the graphql data as an argument, so we can use it on our Layout Component.

As you can see, inside our #navbar-background container we are conditionally rendering our background image depending on the navbar position.

<div id={styles.navbarBackground}>
  {/* navbar background image goes here */}
  {navbarPosition === "vertical" ? (
    <Img
      style={{ height: "100%" }}
      fluid={data.navbarBgVer.childImageSharp.fluid}
    />
  ) : (
    <Img
      style={{ minHeight: "60px" }}
      fluid={data.navbarBgHor.childImageSharp.fluid}
    />
  )}
</div>
Enter fullscreen mode Exit fullscreen mode

Now, our final layout.js component will looks like this.

/**
 * Layout component for all the pages of the site
 */
import React from "react"
import { Link } from "gatsby"
import { StaticQuery, graphql } from "gatsby"
import styles from "./layout.module.css"
import "./layout.css"
import Img from "gatsby-image"

class Layout extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      navbarPosition: this.getPosition(),
    }
  }

  getPosition = () => {
    // This conditional is to avoid errors during build, check
    // https://www.gatsbyjs.org/docs/debugging-html-builds/
    if (typeof window !== "undefined") {
      return window.innerWidth < 580 ? "horizontal" : "vertical"
    }
  }
  componentDidMount() {
    window.addEventListener("resize", this.updateSize)
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.updateSize)
  }
  updateSize = () => {
    let position = this.getPosition()
    if (position !== this.state.navbarPosition) {
      // Changing the state will cause a re-render of the page, do in this case
      // we are only changing the state when the navbar changes position
      this.setState({ navbarPosition: position })
    }
  }

  render() {
    const navbarPosition = this.state.navbarPosition
    return (
      <StaticQuery
        query={graphql`
          {
            navbarBgVer: file(relativePath: { eq: "navbar-ver.jpeg" }) {
              childImageSharp {
                fluid(maxHeight: 1200) {
                  ...GatsbyImageSharpFluid
                }
              }
            }
            navbarBgHor: file(relativePath: { eq: "navbar-hor.jpeg" }) {
              childImageSharp {
                fluid(maxWidth: 2000) {
                  ...GatsbyImageSharpFluid
                }
              }
            }
          }
        `}
        render={data => (
          <div id={styles.layout}>
            <nav id={styles.navbar}>
              <div id={styles.navbarBackground}>
                {/* navbar background image goes here */}
                {navbarPosition === "vertical" ? (
                  <Img
                    style={{ height: "100%" }}
                    fluid={data.navbarBgVer.childImageSharp.fluid}
                  />
                ) : (
                  <Img
                    style={{ minHeight: "60px", width: "110%" }}
                    fluid={data.navbarBgHor.childImageSharp.fluid}
                  />
                )}
              </div>
              <div id={styles.navbarLinkContainer}>
                <Link className={styles.navLink} to={"/"}>
                  HOME
                </Link>
                <Link className={styles.navLink} to={"/about/"}>
                  ABOUT
                </Link>
                <Link className={styles.navLink} to={"/contact/"}>
                  CONTACT
                </Link>
              </div>
            </nav>
            <main id={styles.main}>
              <header id={styles.mainHeader}>
                <h1>{this.props.headerText}</h1>
              </header>
              <div id={styles.content}>{this.props.children}</div>
            </main>
          </div>
        )}
      />
    )
  }
}

export default Layout
Enter fullscreen mode Exit fullscreen mode

My impressions about gatsby.

I ended this small personal project very satisfied with Gatsby, I was looking for a way to port my static personal page to React, and Gatsby not only allowed me to do this, but I also learned about gatsby-image, which I thinks is a great tool for optimizing image rendering. I think that Gatsby will become one of my go-to frameworks for future projects.

I hope this article helps you in your journey, thanks for reading!.

💖 💪 🙅 🚩
fidelve
FidelVe

Posted on October 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related