Creating responsive navbars with background images in GatsbyJS using gatsby-image
FidelVe
Posted on October 3, 2019
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.
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.
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>
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).
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;
}
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
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>
)
}
}
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;
}
}
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() {
//.....//
}
}
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
¦ ¦ }
¦ }
}
}
There are 3 ways of using graphql
with Gatsby, and which one to use, depends on several factors:
- Page queries, that are only to be used in gatsby page components.
- useStaticQuery Hook
- and StaticQuery, which can be used in any component.
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>
)}
/>
)
}
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 ourgraphql
query.The second one is
render
. This one takes a function and passes onto it thegraphql
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>
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
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!.
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
October 3, 2019