Starting with Gatsby + Styled Components + Storybook
Introduction
In this article, I will show you how to use Gatsby with Styled Components and Storybook. GatsbyJS is a framework generator built with React and GraphQL.
GatsbyJS is a static-site generator which helps you build blazing fast websites and apps. To front-end side, I will use Styled Components – the library which allows writing CSS styled in JS. You will also learn about Storybook web development for React – a process of creating a UI development environment that you can use to visualize different states of your UI components.
What Tools You’ll Be Using
- Gatbsy v2 (https://www.gatsbyjs.org)
- Styled Components (https://www.styled-components.com)
- Styled System (https://styled-system.com)
- Storybook v4 (https://storybook.js.org/docs/guides/guide-react/)
Why you should get to know Gatsby?
- Performance – Gatsby sites are couple times faster than similar types of sites
- Popularity – it’s built on React so you got the whole bunch of things that you love about React
- Easy to learn – You can find a lot of well-written tutorials
Want to build your Gatsby project with top developers?
Prerequisites
- Yarn and Node 10+ installed
- Basic understanding of React
Let’s do this!
Step 1: Installation
I will use simple Gatsby Starter Storybook created by markoradak. To create a new project you just clone the starter GitHub repository:
$ git clone https://github.com/markoradak/gatsby-starter-storybook.git
Next, go to the project directory and install packages:
$ cd gatsby-starter-storybook
$ yarn
When the installation is over and the terminal doesn’t show any errors, start your app:
$ yarn start || yarn develop
Gatsby will start a development environment by default at localhost:8000. The server is showing you the starter template:
To show storybook, use command in the another terminal window:
$ yarn run storybook
If Storybook builds successfully, you should be able to navigate to http://localhost:6006 and see the default stories:
The Image which you see is React component (src/components/Image.js) loaded to stories (src/components/Image.stories.js).
How to tell Storybook where to find stories? In .storybook/config.js – this basic configuration automatically imports all files ending in *.stories.js.
Step 2. Create new component and stories
Let’s start with create Header.js component and Header.stories.js.
When I use Storybook, to avoid a mess in my files, I recommend putting React components and stories into directories of the same names.
If you want to do the same with Image component, remember to change his path in index.js and path of SVG image imported to Image.js
Use Styled Components to render components with tags and some styles. Assume that the Header will have two variants: default and light.
Header.js
import React from 'react';
import styled, { css } from 'styled-components';
import PropTypes from 'prop-types';
const HeaderTitleStyled = styled.h1`
font-size: 36px;
font-weight: 600;
color: #fff;
`;
const HeaderStyled = styled.header`
display: flex;
align-items: center;
width: 100%;
background: #744c9d;
padding: 20px 30px;
height: 120px;
${props => props.light && css`
background: #ccc;
${HeaderTitleStyled} {
color: #000;
}
`}
`;
const Header = (props) => {
return (
<HeaderStyled light = {props.light}>
<HeaderTitleStyled>
Gatsby Project + Storybook
</HeaderTitleStyled>
</HeaderStyled>
)
}
Header.propTypes = {
light: PropTypes.bool,
};
export default Header;
Add Header component to index.js and remove unnecessary code from Index page.
import React from 'react';
import styled from 'styled-components';
import Layout from '../components/Layout';
import Header from '../components/Header/Header';
const Page = styled.div`
width: 100%;
max-width: 1260px;
padding: 0 20px;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const IndexPage = () => (
<Layout>
<Page>
<Header/>
</Page>
</Layout>
);
export default IndexPage;
Go to your browser. The default Header should look like:
Open Header.stories.js and write your first story with two variants like this:
import React from 'react';
import { storiesOf } from '@storybook/react';
import Header from './Header'
storiesOf('Header', module)
.add('default', () => (
<Header />
))
.add('light', () => (
<Header light />
));
Now everything is ready. Run your storybook and check the result:
Step 3. How to set global styles in Storybook environment
As you probably noticed the custom font didn’t load to Storybook. This is because the font is defined in Layout.js in App component which is not loaded to Storybook. Located above, there is the GlobalStyle component which is including Styled Reset. These styles are also invisible to Storybook.
So let’s create GlobalStyle.js file in /components directory and move there all global styles. Note that GlobalStyle was generated by using helper function called createGlobalStyle. Normally, styled components are automatically scoped to a local CSS class and therefore isolated from other components. In the case of createGlobalStyle, this limitation is removed and things like CSS resets, or base stylesheets can be applied.
import { createGlobalStyle } from 'styled-components';
import reset from 'styled-reset-advanced';
const GlobalStyle = createGlobalStyle`
${reset};
body {
font-family: 'Avenir Next', 'Helvetica Neue', 'Helvetica', sans-serif;
font-weight: 500;
}
`;
export default GlobalStyle;
In Layout.js, remove GlobalStyle component and import it from GlobalStyle.js. Get rid of unnecessary styles and imports as well.
import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { StaticQuery, graphql } from 'gatsby';
import styled from 'styled-components';
import GlobalStyle from '../components/GlobalStyle'
const App = styled.div``;
...
Now, I will add global styles to Storybook. There are a couple of ways that allow you to set global styles. One that I use is decorator. Open .storybook/config.js and import addDecorator function which will be responsible for applying our add-on to the system. Next, import React and Fragment (which allows for using multiple children without creating extra DOM elements). Last step is creating decorator which is wrapper a story and the GlobalStyle component.
import React, { Fragment } from 'react';
import { configure, addDecorator } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import GlobalStyle from '../src/components/GlobalStyle';
addDecorator((story) => (
<Fragment>
<GlobalStyle/>
{story()}
</Fragment>
));
Global styles are applied to Storybook.
Step 4. Create theme using Theme Provider
A theme is helpful if you would like to keep all variables in one place in a project or you need to create several page themes.
In this step, I will create a theme by wrapping all my components in a ThemeProvider wrapper component, and by referencing the properties of props.theme in our styled-components CSS. Go to the /assets directory and create theme.js file. Next, create theme object with some properties:
theme.js
export const theme = {
color: {
white: '#fff',
black: '#000',
lightGray: '#ececec',
purple: '#744c9d',
},
size: {
inner: '1260px',
}
};
Wrap the Layout component in a ThemeProvider wrapper component:
Note that ThemeProvider doesn’t work like a wrapper – so we have to use Fragment or div tag to keep multiple children inside.
Layout.js
import styled, { ThemeProvider } from 'styled-components';
import theme from '../assets/theme';
const Layout = ({ children }) => (
...
render={data => (
<ThemeProvider theme={theme}>
<Fragment>
<Helmet title={data.site.siteMetadata.title} />
<GlobalStyle />
<App>
{children}
</App>
</Fragment>
</ThemeProvider>
)}
/>
);
And use theme properties in styled-components CSS:
Header.js
const HeaderTitleStyled = styled.h1`
font-size: 36px;
font-weight: 600;
color: ${props => props.theme.color.white};
`;
const HeaderStyled = styled.header`
display: flex;
align-items: center;
width: 100%;
background: ${props => props.theme.color.purple};
padding: 20px 30px;
height: 120px;
${props => props.light && css`
background: ${props => props.theme.color.lightGray};
${HeaderTitleStyled} {
color: ${props => props.theme.color.black}
}
`}
`;
index.js
const Page = styled.div`
width: 100%;
max-width: ${props => props.theme.size.inner};
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
Any change of property in theme.js, will update this property in the component where it was used.
Now my app works fine, but Storybook does not read theme property. To fix it, i’ll open Storybook config and add ThemeProvider to decorator.
import { ThemeProvider } from 'styled-components';
import theme from '../src/assets/theme';
addDecorator((story) => (
<ThemeProvider theme={theme}>
<Fragment>
<GlobalStyle/>
{story()}
</Fragment>
</ThemeProvider>
));
Step 5. Typography – Dynamic Component and Styled System
Regarding typography, first I think about creating a component for each style of text and import it as needed. However, not every text style has the same HTML tag. Preparing several of the same text styles with other tags don’t seem to be the best solution. Fortunately, Dynamic Component allows you to apply any HTML element to component. To create Typography, use Styled System – responsive, theme-based style props for building design systems with React. Styled System works with Styled Component and the most other css-in-js libraries.
First step is to install system-styled via yarn. Please run this command in terminal:
$ yarn add styled-system
Create DynamicComponent.js in /components directory. Import style functions needed from styled-system and add them to the component’s styles argument. This component will have four style props available: fontSize, color, fontWeight and lineHeight.
import React from 'react';
import styled from 'styled-components';
import { fontSize, color, fontWeight, lineHeight } from 'styled-system';
const tag = '';
const StyledDynamicComponent = styled(tag)`
${fontSize}
${color}
${fontWeight}
${lineHeight}
`;
const DynamicComponent = ({ tag = 'p', children, ...props }) => {
const WithComponent = StyledDynamicComponent.withComponent(tag)
return <WithComponent {...props}>{children}</WithComponent>
};
DynamicComponent.defaultProps = {
tag: 'p'
};
export default DynamicComponent;
My DynamicComponent is using withComponent method that creates a new StyledComponent with any tag. By default I set it to paragraph.
Go to theme.js and create textStyles object (in the existing theme object) including styles for your heading and text style. With the ThemeProvider added, the StyledDynamicComponent has access to properties defined in the textStyles object.
breakpoints: ['812px', '1024px'],
textStyles: {
heading1: {
fontSize: ['32px', '42px', '52px'],
color: '#000',
fontWeight: 800,
},
textStyle1: {
fontSize: '16px',
color: '#000',
fontWeight: 400,
lineHeight: '1.4',
},
};
All styled-system functions accept arrays as values to set styles responsively using a mobile-first approach. I defined breakpoints array and fontSize array which will set font-size on the corresponding screen width.
Now it’s time to create a heading and text style component using DynamicComponent. Go to /components/Typography/Typography.js
import React from 'react';
import DynamicComponent from '../DynamicComponent';
import theme from '../../assets/theme';
const {
heading1,
textStyle1
} = theme.textStyles;
export const Heading1 = props => (
<DynamicComponent {...heading1} {...props}>
{props.children}
</DynamicComponent>
);
export const TextStyle1 = props => (
<DynamicComponent {...textStyle1} {...props}>
{props.children}
</DynamicComponent>
);
Let’s render Heading with tag as props and TextStyle with default tag.
index.js
import { Heading1, TextStyle1 } from '../components/Typography/Typography';
const Section = styled.section`
padding: 50px 0;
width: 100%;
`;
const IndexPage = () => (
<Layout>
<Page>
<Header/>
<Section>
<Heading1 tag="h1">This is Heading1 with H1 tag</Heading1>
<TextStyle1>This is TextStyle1 with p tag</TextStyle1>
</Section>
</Page>
</Layout>
);
As a result, you received a responsive component with the ability to control the tag.
You can override your theme styled value or add new property as well.
<TextStyle1 color="#fff" bg="#744c9d">This is TextStyle1 with p tag</TextStyle1>
The result:
Finally, add Typography to stories.
import { Heading1, TextStyle1 } from '../Typography/Typography';
storiesOf('Typography', module)
.add('default', () => (
<Fragment>
<Heading1>Heading1</Heading1>
<TextStyle1>TextStyle1</TextStyle1>
</Fragment>
));
And check your Storybook:
Step 6. Make your app responsive
The easiest way to build a responsive app is by using media queries like in normal CSS.
const HeaderStyled = styled.header`
background: ${props => props.theme.color.black};
@media only screen and (min-width: 1024px) {
background: ${props => props.theme.color.purple};
}
`;
However, let’s try to make media queries more dynamic. Set breakpoints values in mediaQueries object in theme.js and then use them in breakpoint object in new created /components/MediaQueries.js
theme.js
export const theme = {
...
mediaQueries: {
tablet: '1024px',
phone: '812px',
},
};
MediaQueries.js
import theme from '../assets/theme';
const {
tablet,
phone
} = theme.mediaQueries;
export const bp = {
tablet: `min-width: ${tablet}`,
phone: `min-width: ${phone}`,
};
Change CSS media query to an property of a breakpoint object.
const HeaderStyled = styled.header`
background: ${props => props.theme.color.black};
@media (${bp.tablet}) {
background: ${props => props.theme.color.purple};
}
`;
Step 7. Gatbsy Image
One of the most important advantages of Gatsby is it’s work very fast. Many delays in loading pages are caused by too big and non-optimized images. Gatbsy has several useful plugins that help completely optimize images and maintain great performance on our website. The recommended approach is to use gatbsy-image – React component designed to work with Gatsby’s GraphQL queries. It generates multiple sizes and resolutions of each image so you don’t load large images on a mobile device. You have already installed gatsby-image and others needed plugins (gatsby-transformer-sharp, gatsby-plugin-sharp and gatsby-source-filesystem) in the starter (take a look package.json).
Add some images to /assets/images. You can download beautiful and free images from https://unsplash.com.
Navigate to the gatsby-config.js and set up a source-filesystem plugin so that your images are available in image queries.
{
resolve: 'gatsby-source-filesystem',
options: {
path: `${__dirname}/src/assets/images`,
name: 'images',
},
},
Gatbsy-image supports two types of responsive images:
- fixed (images that have fixed width and height)
- fluid (images that stretch when the page is resized).
Here you find more info about the type of responsive images.
This example shows a gallery of two images using two methods:
index.js
import React from 'react';
import styled from 'styled-components';
import { graphql } from 'gatsby';
import Img from 'gatsby-image';
...
const Image = styled.div`
max-width: 5000px;
margin: 20px 0;
`
const IndexPage = ( {data} ) => (
<Layout>
<Page>
<Header/>
<Section>
<Heading1 tag="h1">This is Heading1 with H1 tag</Heading1>
<TextStyle1>This is TextStyle1 with p tag</TextStyle1>
<Image>
<Img fluid={data.pictureOne.childImageSharp.fluid} />
</Image>
<Img fixed={data.pictureTwo.childImageSharp.fixed} />
</Section>
</Page>
</Layout>
);
export const query = graphql`
query {
pictureOne: file(relativePath: { eq: "picture-1.jpg" }) {
childImageSharp {
fluid(maxWidth: 500, quality: 100) {
...GatsbyImageSharpFluid
}
}
}
pictureTwo: file(relativePath: { eq: "picture-2.jpg" }) {
childImageSharp {
fixed(width: 500, quality: 100) {
...GatsbyImageSharpFixed
}
}
}
};
`
export default IndexPage;
The result is:
Gatbsy-image lazy loads your images with a pretty nice “blur-up” effect. If you don’t want to use this effect, you can experiment with others value at the end of fragment.
To learn more, read the Fragments section https://www.gatsbyjs.org/packages/gatsby-image/
For example:
pictureOne: file(relativePath: { eq: "picture-1.jpg" }) {
childImageSharp {
fluid(maxWidth: 500, quality: 100) {
...GatsbyImageSharpFluid_noBase64
}
}
}
pictureTwo: file(relativePath: { eq: "picture-2.jpg" }) {
childImageSharp {
fixed(width: 500, quality: 100) {
...GatsbyImageSharpFixed_tracedSVG
}
}
}
Images are loaded with other effects.
Remember that gatbsy-image doesn’t support GIF and SVG images. To use GIF image, Gatsby recommends to import the image directly. In the case of SVG, creating multiple variants of the image doesn’t make sense because it is vector-based graphics that you can freely scale without losing quality.
Conclusion
As you can see, work with Gatsby is pleasant, and the benefits of using it are really great. Hope you enjoyed this article and got inspired to broaden your knowledge about Gatsby 🙂 I encourage you to dive into more documentation and tutorials on https://www.gatsbyjs.org/docs/.
comments
lucadavidson
Posted on
Thanks on your marvelous posting! I quite enjoyed reading
it, you are a great author.I will make certain to bookmark your blog and will come back later on. I want to encourage you to ultimately continue your
great job, have a nice holiday weekend!
wesam
Posted on
This article is very clear and easy to understand Gatsby, especially if you are a beginner with Gatsby, indeed encourage you to ultimately continue your great job, thank you so much
Comments are closed.