Storybook & Atomic Design – 1.11. – Building Our Header

In this lesson, we'll be building our Header component. The Header component falls under the concept of an organism, as it could contain molecules and atoms as children nodes.

Initially, we will focus on passing one prop to our Header component. The prop will be -

  1. navigation - array

For each array item in our navigation prop we can expect an object with the following key -

  1. items - array
  2. title - string

For each array item in our items prop we can expect an object with the following keys -

  1. icon - string
  2. title - string
  3. url - string

Creating the Component Files

We will want to create the Header component in a new directory under components, organisms then Header . This directory will house the component itself, the associated stories, emulated JSON payload data, and styled-components logic.

- __Storybook__
   - __components__
     - __organisms__
       - __header__
         - header.jsx
         - header.knobs.json
         - header.stories.js
         - header.styles.jsx

header.styles.jsx

Before we get started on the markup required to generate our Header component elements, let's first import some styles to make our front-end implementation align with the Figma designs I've provided at the start of the course.

import styled from "styled-components"

const headerColour = props => {
  // Fallback value if we can't get access to props
  if (!props || !props.theme || !props.theme.black) return "#131313"

  // If no variant is specified, return the white colour
  if (!props.variant) return props.theme.black

  // Dynamically determine the background colour based on props
  let colour
  switch (props.variant) {
    case "fixedLight":
      colour = props.theme.white
      break
    case "fixedDark":
      colour = props.theme.black
      break
    default:
      colour = props.theme.black
      break
  }

  return colour
}

const headerPosition = props => {
  // Fallback value if we can't get access to props
  if (!props || !props.variant) return "relative"

  // Dynamically determine the background colour based on props
  let position
  switch (props.variant) {
    case "fixedLight":
      position = "absolute"
      break
    case "fixedDark":
      position = "absolute"
      break
    default:
      position = "relative"
      break
  }

  return position
}

export const StyledHeader = styled.header`
  align-items: center;
  display: flex;
  left: ${props => (props.variant ? `0` : undefined)};
  padding: 30px;
  position: relative;
  top: ${props => (props.variant ? `0` : undefined)};
  width: 100%;
  z-index: 9;

  color: ${props => headerColour(props)};

  @media (min-width: 992px) {
    display: block;
    padding: 0;
    position: ${props => headerPosition(props)};
  }

  button {
    display: inline-flex;
    margin-left: auto;

    @media (min-width: 992px) {
      display: none;
    }
  }

  img {
    height: 40px;

    @media (min-width: 992px) {
      height: 64px;
      left: 50%;
      position: absolute;
      top: 50%;

      transform: translate(-50%, -50%);
    }
  }

  nav {
    padding: 0;
  }

  nav + nav {
    margin-left: auto;
  }

  svg {
    height: 24px;
    stroke: 1px solid ${props => props.theme.black};
  }

  .header__navigation {
    align-items: center;
    display: block;
    height: 100%;
    padding: 124px 30px 30px;
    position: fixed;
    left: 0;
    top: -100%;
    width: 100%;
    z-index: -1;

    background-color: ${props => props.theme.offWhite};
    color: ${props => props.theme.black};
    transition: 0.4s top ease;

    a + a {
      margin-left: 0;
      margin-top: 16px;

      @media (min-width: 992px) {
        margin-left: 32px;
        margin-top: 0;
      }
    }

    nav {
      padding-top: 24px;
      flex-direction: column;

      border-top: 1px solid ${props => props.theme.grey600};

      @media (min-width: 992px) {
        flex-direction: unset;
        padding-top: 0;

        border-top: none;
      }
    }

    nav + nav {
      margin-top: 24px;

      @media (min-width: 992px) {
        margin-top: 0;
      }
    }

    @media (min-width: 992px) {
      display: flex;
      left: unset;
      margin: 0 auto;
      max-width: 1920px;
      min-height: 124px;
      padding: 30px;
      position: relative;

      background-color: transparent;
      color: inherit;
    }
  }

  &.header--open {
    .header__navigation {
      top: 0%;

      @media (min-width: 992px) {
        left: unset;
      }
    }
  }
`

export default StyledHeader

header.jsx

Our Header component is made up of three sub-components, a navigation menu on the left, a logo image in the center and a navigation menu on the right.

Our Header component accepts one prop named navigation which is an array of objects that we can map over to return several instance of our Navigation molecule component within our Header component.

First we will start by destructuring the accepted props passed to our Header component and extract the named value of navigation.

We start our component by referencing our StyledHeader as a parent wrapper of the component HTML elements and React child components.

If navigation is defined and iterable, the Header component will loop over each node it finds, in this case, we have objects with a single key items. We destructure this value immediately in our array map function and pass them into a return statement.

Next, we return our Navigation molecule with the items (navigation items) value passed as a prop.

import React from "react";

import StyledHeader from "./header.styles";

import Button from "../../atoms/button/button";

import Navigation from "../../molecules/navigation/navigation";

const Header = ({ navigation }) => (
	<StyledHeader>
		<div className="header__navigation">
			{navigation.length > 0 &&
				navigation.map(({ items, title }) => (
					<Navigation items={items} key={`header-menu-${title}`} />
				))}
		</div>
	</StyledHeader>
);

export default Header;

So far we've been working with SVG images only, and in an ideal world we would have the logo available to us as an SVG. However, I've been provided the logo as a PNG file and whilst it's possible to convert this simple shape into a vector, I thought it'd be good to show you how we can use raster images in our React application.

First we'll import the asset as we would any other media file in our Header component.

You'll notice we've included the image as the last element in our Header, this is because I believe the navigation should take priority for screen readers and accessible tools, with flexbox and CSS properties styling the layout order only for browsers.

import React from 'react';

import StyledHeader from "./header.styles"

import Logo from "../../../assets/images/logo.png";

import Button from "../../atoms/button/button";

import Navigation from "../../molecules/navigation/navigation";

const Header = ({navigation}) => (
	<StyledHeader>
		<div className="header__navigation">
			{navigation.length > 0 &&
				navigation.map(({ items, title }) => (
					<Navigation items={items} key={`header-menu-${title}`} />
				))}
		</div>
		<img src={Logo} alt="Celtic Elements Logo" />
	</StyledHeader>
);

export default Header;

useState

In our React application, there are going to be several instances where we reference a global state store to manage our shopping cart quantities, shipping information and other bits of information we want accessible globally to our application.

However, there are some instances where we want to isolate state to a single component.

The Header component is one of those cases. I want to include a button which will allow us to toggle a className for our StyledHeader component, allowing us to show and hide the navigation menus on a mobile device.

Think of a hamburger menu but with a label.

To achieve this, we'll be using React hooks.

Hooks are a fairly new concept in React and allow us to write functional components which can still use the benefits class components once provided us without the mess.

To get started, we'll first extend our React imports to include the useState hook.

import React, { useState } from 'react';
import { arrayOf, shape, string } from "prop-types";

With our new hook available, we'll need to rewrite our Header component to use a scope opener, instead of returning our StyledHeader element immediately, this is so we can set up the hook to be used within our component.

We then use [currentValue, setValue] as an array destructure to name the two values coming out of our useState function.

Finally, we pass an initial value to our useState hook itself. This is the value that the currentValue will equal when the React component is first mounted to the DOM.

const Header = ({navigation}) => {
	const [isOpen, setOpen] = useState(false);

	return (
		<StyledHeader>
			{navigation && navigation.length && navigation.map(({items}) => (
				<Navigation items={items} />
			))}
			<img alt="Celtic Elements Logo" className="header__logo" src={BrandLogo} />
		</StyledHeader>
	);
}

Creating Our Header Hamburger ?

With our new state variables available to our Header component, we can now build our Button to toggle the className applied to our StyledHeader.

Before we set about creating the HTML button itself, let's create the functionality the button needs. I've created a function named toggleNavigation that accepts an event as an argument. The function prevents the default functionality of the button event, stopping any unexpected behaviour in our function.

We then use the setOpen function we destructured from our useState hook to invert the value of isOpen. So when the button is first clicked, it will take the value of false and invert it to true. Once clicked again it will do the opposite (true to false).

Next, we use a ternary operator to query isOpen when assigning a className to our StyledHeader React component. If the value of isOpen is true then we return the className 'header--open', if the value is false then we return 'header--closed'.

Read more about ternary operators at - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator

Finally, we'll create a button the precedes the navigation menus, allowing those on mobile devices using screen assisted tools to access the button before any other Header element.

We then assign the function we've created to the onClick event of the button. The button element is smart enough to know to pass the event of onClick to the function so we don't need to set up any references there.

const Header = ({ navigation }) => {
	const [isOpen, setOpen] = useState(false);

	const toggleMenu = e => {
		e.preventDefault();
		setOpen(!isOpen);
	};

	return (
		<StyledHeader className={isOpen ? `header--open` : `header--closed`}>
			<div className="header__navigation">
				{navigation.length > 0 &&
					navigation.map(({ items, title }) => (
						<Navigation items={items} key={`header-menu-${title}`} />
					))}
			</div>
			<img src={Logo} alt="Celtic Elements Logo" />
			<Button onClick={toggleMenu}>{isOpen ? `Hide` : `Show`} menu</Button>
		</StyledHeader>
	);
};

Alternatively, you could implement the following, although I think the above looks cleaner.

const Header = ({ navigation }) => {
	const [isOpen, setOpen] = useState(false);

	const toggleMenu = e => {
		e.preventDefault();
		setOpen(!isOpen);
	};

	return (
		<StyledHeader className={isOpen ? `header--open` : `header--closed`}>
			<div className="header__navigation">
				{navigation.length > 0 &&
					navigation.map(({ items, title }) => (
						<Navigation items={items} key={`header-menu-${title}`} />
					))}
			</div>
			<img src={Logo} alt="Celtic Elements Logo" />
			<Button
				onClick={e => {
					e.preventDefault();
					setOpen(!isOpen);
				}}>
					{isOpen ? `Hide` : `Show`} menu
			</Button>
		</StyledHeader>
	);
};

Adding Icons to Our Navigation

You may have noticed that in our designs, we have SVG icons in place of two navigation items on the right-hand side of the Header component.

To implement these, we'll need to revisit our Navigation molecule first and add support for a new prop value of icon.

In the updated Navigation component I've -

  1. Imported the icons I want to make available to our component
  2. Assigned the icon assets to an Icons object with key names
  3. Created a new sub-component named NavigationIcon which accepts all navigation item keys as props and spreads them into the component as props
  4. Created the NavigationIcon sub-component which returns a React Fragment holding the icon and a visibly hidden text label to describe the navigation item to screen readers.
import React from "react";
import { arrayOf, shape, string } from "prop-types";

import StyledNavigation from "./navigation.styles";

import IconBag from "../../../assets/images/icons/shopping-bag.svg";
import IconCart from "../../../assets/images/icons/shopping-cart.svg";
import IconPlus from "../../../assets/images/icons/plus.svg";
import IconUser from "../../../assets/images/icons/user.svg";
import IconX from "../../../assets/images/icons/x.svg";

const Icons = {
	bag: IconBag,
	cart: IconCart,
	plus: IconPlus,
	user: IconUser,
	x: IconX
};

const Navigation = ({ direction, items }) => (
	<StyledNavigation direction={direction}>
		{items.map(item => (
			<a href={item.url}>
				{item.icon ? (
					<NavigationIcon name={item.icon} title={item.title} />
				) : (
					item.title
				)}
			</a>
		))}
	</StyledNavigation>
);

// Expected prop values
Navigation.propTypes = {
	direction: string.isRequired,
	items: arrayOf(
		shape({
			icon: string,
			title: string.isRequired,
			url: string.isRequired
		})
	)
};

// Default prop values
Navigation.defaultProps = {
	direction: "horizontal",
	items: []
};

const NavigationIcon = ({ name, title }) => {
	// If icon name value doesn't match Icons object keys then return null
	if (Icons[name] === undefined) return null;
	// If icon found, return the icon in a span element
	const Icon = Icons[name];
	return (
		<span className="navigation__icon">
			{title && <span className="hidden">{title}</span>}
			<Icon />
		</span>
	);
};

// Navigation Icon component always expects on prop value for icon name
NavigationIcon.propTypes = {
	name: string.isRequired
};

export default Navigation;

header.knobs.json

As we don't have any data-source for our component, we will use a JSON file to emulate the data we would expect from a JSON payload.

We can also make use of our Knobs add-on to create an array input field in our story, allowing us to update the data when previewing the component.

{
	"navigation": {
		"default": [
			{
				"title": "general",
				"items": [
					{
						"icon": null,
						"title": "Shop",
						"url": "#"
					},
					{
						"icon": null,
						"title": "About Celtic Elements",
						"url": "#"
					},
					{
						"icon": null,
						"title": "FAQ",
						"url": "#"
					},
					{
						"icon": null,
						"title": "Contact",
						"url": "#"
					}
				]
			},
			{
				"title": "account",
				"items": [
					{
						"icon": null,
						"title": "Insights",
						"url": "#"
					},
					{
						"icon": null,
						"title": "Account",
						"url": "#"
					},
					{
						"icon": "user",
						"title": "User",
						"url": "#"
					},
					{
						"icon": "bag",
						"title": "Cart",
						"url": "#"
					}
				]
			}
		],
		"group": "Content",
		"label": "Header navigation"
	}
}

PropTypes

To keep consistency with our other components and to ensure the Header component accepts a valid array of values, we will define the propTypes our component should expect.

Most of the configuration you find here can be duplicated from our Footer component lesson.

import React, { useState } from "react";
import { arrayOf, shape, string } from "prop-types";

import StyledHeader from "./header.styles";

import Logo from "../../../assets/images/logo.png";

import Button from "../../atoms/button/button";

import Navigation from "../../molecules/navigation/navigation";

const Header = ({ navigation }) => {
	const [isOpen, setOpen] = useState(false);

	const toggleMenu = e => {
		e.preventDefault();
		setOpen(!isOpen);
	};

	return (
		<StyledHeader className={isOpen ? `header--open` : `header--closed`}>
			<div className="header__navigation">
				{navigation.length > 0 &&
					navigation.map(({ items, title }) => (
						<Navigation items={items} key={`header-menu-${title}`} />
					))}
			</div>
			<img src={Logo} alt="Celtic Elements Logo" />
			<Button onClick={toggleMenu}>{isOpen ? `Hide` : `Show`} menu</Button>
		</StyledHeader>
	);
};

// Expected prop values
Header.propTypes = {
	navigation: arrayOf({
		items: arrayOf(
			shape({
				icon: string,
				title: string.isRequired,
				url: string.isRequired
			})
		),
		title: string
	})
};

// Default prop values
Header.defaultProps = {
	navigation: []
};

export default Header;

header.stories.js

We can then import the JSON payload in our Header Storybook story and configure the array knob to give viewers of the component an input field to manipulate the prop data fed to the Header.

import React from "react";
import { withKnobs, array } from "@storybook/addon-knobs";

import Header from "./header";

import knobData from "./header.knobs.json";
const { navigation } = knobData;

export const standardHeader = () => (
	<Header
		navigation={array(navigation.label, navigation.default, navigation.group)}
	/>
);

export default {
	component: Header,
	decorators: [withKnobs],
	title: "Organisms|Header"
};

Conclusion

Now we have a type defined Header component which makes use of our smaller component concepts (Navigation molecule) and dynamically renders navigation lists based on the prop data available to the component.

Not only this but we've touched on our first React hook, allowing us to make use of the state functionality React is praised for. Giving us the ability to toggle the Header components class which will create a hamburger navigation toggle on mobile devices.

In the next lesson, we'll be covering how we can build a blog post template, using several molecules and organisms to construct a complete page layout.

Continue Reading 📚