As an assignment for an app, I needed to implement a more consistent typography/theme. Consistency through your app is essential, and it also saves you much time. So instead of refactoring those buttons to set your font family, your margins, or your primary color, you can start with this and not worry about it anymore. There were numerous articles on how to approach this, and it's not something new, but writing about it seems like an excellent exercise to put what I've learned to practice. While reading these articles, I came across styled-system, an impressive library that makes it easy to make props available for your components. This library will be the foundation of this article. The articles will be listed at the end of the article.
I hope you enjoy this step by step tutorial on how to set up a style guide foundation for your app. We are going to do everything from scratch, starting with creating your app up until the style guide showcase in storybook. Let's get started!
If you don't fancy reading through the setup because you've done this a thousand times, here is how this article is written:
How this article is written:
- Setup
- Theme structure
- Dynamic component
- Setting up the typography styles and components
- Reference story in Storybook
- Wrapping up
- Sources
Setup
Creating the app and installing extra packages
The create react app is a great start for a react app, we will use the Typescript flag to get all the nice features typescript gives us (especially those typescript 3.7 features)
npx create-react-app react-styleguide --typescript
Then we will install the other packages we need:
yarn add styled-components @types/styled-components styled-system @types/styled-system react-router-dom
Next we can add our dev dependencies:
yarn add -D prettier eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-prettier eslint-plugin-prettier
The last thing we have to install is storybook. Storybook will be used to create a reference showcase of our theme ( typography, colors, space, breakpoints).
npx -p @storybook/cli sb init --type react
Storybook is a convenient tool you can use to prototype your components. I wrote an article about Storybook Driven Development a while ago that also introduces the basics of storybook. You can read it here.
Prettier & Eslint config
We want our code to be nice and neat, so we are going to use prettier and eslint to keep us in our lanes. These were already installed in the steps before, but now we have to configure them.
Our eslint (.eslintrc) setup will look something like this:
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"prettier/prettier": [
"error",
{
"singleQuote": true
}
],
"react/prop-types": "off",
"@typescript-eslint/no-explicit-any": "error"
},
"settings": {
"react": {
"version": "detect"
}
}
}
And prettier (.prettierrc) like this:
{
"trailingComma": "es5",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 140
}
Both these files need to be created in the root of your project.
Storybook setup
Luckily for us, Storybook now works out of the box with Typescript. We just have to setup up a few things for future use. We will setup our ThemeProvider and the Router ( for Links ). You can copy-paste the code below in your config.js in the .storybook folder.
import { addDecorator, configure } from '@storybook/react';
import { ThemeProvider } from 'styled-components';
import React from 'react';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
addDecorator(story => (
<ThemeProvider theme={{}}>
<Router history={history}>{story()}</Router>
</ThemeProvider>
));
configure(require.context('../src', true, /\.stories\.tsx$/), module);
We do a few things in this file:
- Setup the
ThemeProvider
with an empty object for now, because we don't have a theme yet - Setup our
Router
, so ourLink
component doesn't break in storybook - Change the
context
function, so it picks up our.tsx
files. - Change the folder where our
context
function searches for stories ( I like to keep my stories together with the components)
Next, we can change the extension of the demo story that storybook provides (0-Welcome.stories.js ) from js
to tsx
and move it in our src
folder to see if everything is working. Try running yarn storybook
, and if you see the screen below, you have your storybook setup done. Good job! 🔥
If you don't see this, don't worry, I got you. You can just clone this branch I made as a checkpoint branch so you can continue to follow the tutorial
Now we are ready to get our hands dirty, let's start with our theme structure!
Making our Theme structure
Our theme is the skeleton of the app, this is where we define our standards ( most of them ) and how these should be used. We are heavily relying on styled-system for this. There are a few things we want to define in our theme:
- Space
- Breakpoints ( for responsive fonts )
- Colors
Let's start by making a folder in our src folder called styleguide. In this folder, we can create a defaulTheme.ts
. In this file, we will define our theme. This theme will have the structure of the type provided by styled-system
. You can read the specification of the theme here
We won't be using all these properties for this article, as it would just be too much work and probably a bit boring to read. So we are going to keep it simple by only setting up space, breakpoints, and colors.
Space
The point of defining your space is to avoid having inconsistent pixels all over the place in your app. With predefined space values, you are going to have more predictable results. You can set this up however you want, but I like the geometric progression explained in this article combined with the t-shirt sizes approach. It looks something like this:
export interface Space {
NONE: number;
XS: number;
S: number;
M: number;
L: number;
XL: number;
XXL: number;
}
export const space: Space = {
NONE: 0,
XS: 2,
S: 4,
M: 8,
L: 16,
XL: 32,
XXL: 64,
};
Breakpoints
Next are our breakpoints. These breakpoints are mostly used with responsive fonts, I'll show it works later.
export const breakpoints: string[] = ['319px', '424px', '767px', '1023px'];
Colors
These colors are highly opinionated, and you can set up your colors the way you want. I'll just give you an idea of how to set them up. Notice that I am using the csstype that styled-system uses.
import * as CSS from 'csstype';
export interface ThemeColors {
primary: CSS.ColorProperty;
link: CSS.ColorProperty;
success: CSS.ColorProperty;
warning: CSS.ColorProperty;
error: CSS.ColorProperty;
heading: CSS.ColorProperty;
text: CSS.ColorProperty;
disabled: CSS.ColorProperty;
border: CSS.ColorProperty;
}
export const colors: ThemeColors = {
primary: '#423EA2',
link: '#1890ff',
success: '#52c41a',
warning: '#faad14',
error: '#e84118',
heading: '#423EA2',
text: '#000',
disabled: '#f5222d',
border: '#423EA2',
};
At last, our defaultTheme.ts:
import { Theme } from 'styled-system';
import { colors } from './colors';
import { space } from './space';
export const breakpoints: string[] = ['319px', '424px', '767px', '1023px'];
export const defaultTheme: Theme = {
space: {
...space,
},
breakpoints,
colors: {
...colors,
},
};
Once you define these you can use them in components like this:
const Button = styled.button`
background-color: ${({ theme }) => theme.colors.primary};
padding: ${({ theme }) => theme.space.M}px;
margin: ${({ theme }) => theme.space.M}px;
width: 200px;
border: none;
color: white;
font-size: 14px;
font-weight: 700;
border-radius: 15px;
letter-spacing: 2px;
text-transform: uppercase;
`;
And the result :
Creating our dynamic styled component
Next up, we will be making our dynamic styled component with all the styled-system props built-in. Here is were styled-system shines as we only have to use the style functions they provide.
import React from 'react';
import styled from 'styled-components';
import {
borderRadius,
BorderRadiusProps,
color,
fontFamily,
FontFamilyProps,
fontSize,
FontSizeProps,
fontStyle,
FontStyleProps,
fontWeight,
FontWeightProps,
letterSpacing,
LetterSpacingProps,
lineHeight,
LineHeightProps,
size,
SizeProps,
space,
SpaceProps,
textAlign,
TextAlignProps,
textStyle,
TextStyleProps,
} from 'styled-system';
export type StyledSystemProps =
| SpaceProps
| FontSizeProps
| FontStyleProps
| SizeProps
| TextStyleProps
| LetterSpacingProps
| FontFamilyProps
| FontWeightProps
| BorderRadiusProps
| FontFamilyProps
| LineHeightProps
| TextAlignProps
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| { color: string; as?: keyof JSX.IntrinsicElements | React.ComponentType<any> };
export default styled.div`
${space}
${fontSize}
${fontStyle}
${size}
${color}
${textStyle}
${letterSpacing}
${fontFamily}
${fontWeight}
${borderRadius}
${lineHeight}
${textAlign}
`;
This means that this component now has a bunch of props ready to be used for easy styling. This dynamic component will be the foundation for setting up all our typography types.
Setting up all the typography styles and components
Now we can start creating our text styles. I am not an expert on typography, but these guidelines from BBC will give you a good introduction to typography harmony. Our text styles will look like this:
import { colors } from './colors';
import { StyledSystemProps } from './DynamicStyledSystemComponent';
const fontFamilies: { heading: string; body: string } = {
heading: 'Montserrat, serif',
body: 'Raleway, sans-serif',
};
interface TypographyStyles {
H1: StyledSystemProps;
H2: StyledSystemProps;
H3: StyledSystemProps;
H4: StyledSystemProps;
H5: StyledSystemProps;
LargeLead: StyledSystemProps;
SmallLead: StyledSystemProps;
Paragraph: StyledSystemProps;
SmallParagraph: StyledSystemProps;
Link: StyledSystemProps;
}
export const typographyStyles: TypographyStyles = {
H1: {
fontSize: [50, 51, 52, 57],
fontWeight: 700,
fontFamily: fontFamilies.heading,
as: 'h1',
},
H2: {
fontSize: [37, 39, 41, 43],
fontWeight: 700,
color: colors.primary,
fontFamily: fontFamilies.heading,
as: 'h2',
},
H3: {
fontSize: [27, 28, 30, 32],
fontWeight: 700,
fontFamily: fontFamilies.heading,
as: 'h3',
},
H4: {
fontSize: [18, 20, 22, 24],
fontWeight: 700,
color: colors.primary,
fontFamily: fontFamilies.heading,
as: 'h4',
},
H5: {
fontWeight: 700,
fontSize: [16, 17, 19, 21],
fontFamily: fontFamilies.heading,
as: 'h5',
},
LargeLead: {
fontWeight: 500,
fontSize: [18, 20, 22, 24],
fontFamily: fontFamilies.heading,
as: 'p',
},
SmallLead: {
fontWeight: 500,
fontSize: [17, 18, 19, 21],
fontFamily: fontFamilies.heading,
as: 'p',
},
Paragraph: {
fontSize: [14, 15, 15, 16],
fontWeight: 300,
fontFamily: fontFamilies.body,
as: 'p',
},
SmallParagraph: {
fontSize: [13, 14, 14, 15],
fontWeight: 300,
fontFamily: fontFamilies.body,
as: 'p',
},
Link: {
fontWeight: 700,
color: colors.primary,
fontSize: [14, 15, 15, 16],
fontFamily: fontFamilies.body,
},
};
With these text styles, we can create typography components. We can create a helper function createComponent that has two arguments: the corresponding styling props, and it's display name. The Link component is not created with the createComponent function because it needs to use the react-dom Link component.
import React from 'react';
import { Link as RouterLink, LinkProps } from 'react-router-dom';
import DynamicStyledSystemComponent, { StyledSystemProps } from './DynamicStyledSystemComponent';
import { typographyStyles } from './typographyStyles';
type AnchorProps = StyledSystemProps & Pick<LinkProps, 'to'> & { onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void };
const Link: React.FC<AnchorProps> = ({ to, onClick, children, ...props }) => (
<RouterLink to={to} onClick={onClick}>
<DynamicStyledSystemComponent {...typographyStyles.Link} {...props}>
{children}
</DynamicStyledSystemComponent>
</RouterLink>
);
interface TypographyComponentProps {
H1: React.FC<StyledSystemProps>;
H2: React.FC<StyledSystemProps>;
H3: React.FC<StyledSystemProps>;
H4: React.FC<StyledSystemProps>;
H5: React.FC<StyledSystemProps>;
LargeLead: React.FC<StyledSystemProps>;
SmallLead: React.FC<StyledSystemProps>;
Paragraph: React.FC<StyledSystemProps>;
SmallParagraph: React.FC<StyledSystemProps>;
Link: React.FC<AnchorProps>;
}
const createComponent: (textStyle: StyledSystemProps, displayName: string) => React.FC<StyledSystemProps> = (textStyle, displayName) => {
const component: React.FC<StyledSystemProps> = props => (
<DynamicStyledSystemComponent {...textStyle} {...props}>
{props.children}
</DynamicStyledSystemComponent>
);
component.displayName = displayName;
return component;
};
export const Typography: TypographyComponentProps = {
H1: createComponent(typographyStyles.H1, 'H1'),
H2: createComponent(typographyStyles.H2, 'H2'),
H3: createComponent(typographyStyles.H3, 'H3'),
H4: createComponent(typographyStyles.H4, 'H4'),
H5: createComponent(typographyStyles.H5, 'H5'),
LargeLead: createComponent(typographyStyles.LargeLead, 'LargeLead'),
SmallLead: createComponent(typographyStyles.SmallLead, 'SmallLead'),
Paragraph: createComponent(typographyStyles.Paragraph, 'Paragraph'),
SmallParagraph: createComponent(typographyStyles.SmallParagraph, 'SmallParagraph'),
Link: Link,
};
Now we can start using our Typography components. I will showcase this next in storybook.
Reference story in Storybook
We can showcase how our styleguide is setup by making a storybook story. This will serve as a reference if you ever want to know which Typography, color, space you want to use.
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Typography } from './Typography';
import styled from 'styled-components';
import { colors } from './colors';
import { breakpoints } from './theme';
import { space } from './space';
const { H1, H2, H3, H4, H5, LargeLead, Link, Paragraph, SmallLead, SmallParagraph } = Typography;
storiesOf('Styleguide ', module)
.addParameters({ viewport: { defaultViewport: 'default' } })
.add('default', () => (
<Container>
<H2>Typography</H2>
<Divider />
<H1>H1: Animi aperiam, aspernatur culpa deserunt eaque, eius explicabo inventore ipsa laudantium</H1>
<H2>H2: Consectetur consequuntur cum deserunt dignissimos esse fugiat inventore iusto, laboriosam maiores minima!.</H2>
<H3>H3: Culpa dignissimos expedita facilis, fugiat minus odio reiciendis ut? Accusamus delectus dicta eius.</H3>
<H4>H4: Accusamus ad adipisci alias aliquam aperiam autem, culpa dolorem enim error est eum.</H4>
<H5>H5: Debitis distinctio dolorum fugiat impedit itaque necessitatibus, quo sunt? Atque consectetur, corporis.</H5>
<LargeLead>LargeLead:Deleniti est facere id placeat provident sapiente totam vitae. Asperiores consequuntur eaque eum.</LargeLead>
<SmallLead>SmallLead: At aut corporis culpa doloribus ea enim error est impedit, ipsum iure maxime molestiae omnis optio.</SmallLead>
<Paragraph>
Paragraph: Facilis hic iste perspiciatis qui quibusdam sint velit vero Animi doloremque esse ex iure perferendis.
</Paragraph>
<SmallParagraph>SmallParagraph: Ad animi at debitis eligendi explicabo facere illum inventore, ipsum minus obcaecati.</SmallParagraph>
<Link to="/">Link: Lorem ipsum dolor sit amet, consectetur adipisicing elit.</Link>
<Divider />
<H2>Colors</H2>
<Paragraph>These colors are defined in styleguide colors.ts.</Paragraph>
<Divider />
<GridContainer>
<div>
<SmallParagraph>Kind</SmallParagraph>
</div>
<div>
<SmallParagraph>HEX</SmallParagraph>
</div>
<div>
<SmallParagraph>Color</SmallParagraph>
</div>
</GridContainer>
{Object.entries(colors).map(obj => (
<GridContainer key={obj[0]}>
<SmallParagraph>{obj[0]}</SmallParagraph>
<SmallParagraph>{obj[1]}</SmallParagraph>
<ColorCircle color={obj[1]} />
</GridContainer>
))}
<Divider />
<H2>Breakpoints</H2>
<Paragraph>These are the responsive breakpoints being used</Paragraph>
<br />
<FlexContainer>
{breakpoints.map((key: string) => (
<SmallParagraph key={key} m={4}>
{key}
</SmallParagraph>
))}
</FlexContainer>
<Divider />
<H2>Space</H2>
<FlexContainer>
{Object.entries(space).map(obj => (
<div key={obj[0]}>
<SmallParagraph m={2}>
<strong>{obj[1]}px</strong>
</SmallParagraph>
<SmallParagraph m={2}>{obj[0]}</SmallParagraph>
</div>
))}
</FlexContainer>
</Container>
));
const Divider = styled.div`
border: 1px solid #00000022;
width: 100%;
margin: ${({ theme }) => theme.space.M}px;
`;
const ColorCircle = styled.div<{ color: string }>`
height: 20px;
width: 20px;
border-radius: 20px;
background-color: ${({ color }) => color};
`;
const GridContainer = styled.div`
display: grid;
grid-template-columns: 150px 150px 150px;
margin: ${({ theme }) => theme.space.M}px;
`;
const FlexContainer = styled.div`
display: flex;
`;
const Container = styled.div`
background-color: white;
height: 100vh;
padding: 16px;
`;
Wrapping up
So this was my take on how to setup a styleguide for you next react app. Hope you enjoyed the post and until the next one! Cheers
Sources
https://levelup.gitconnected.com/building-a-react-typography-system-f9d1c8e16d55
https://www.bbc.co.uk/gel/guidelines/typography
https://sproutsocial.com/seeds/visual/typography/
https://medium.com/eightshapes-llc/space-in-design-systems-188bcbae0d62
Top comments (11)
It's great that you have a variety of props you can use now to define your component but now your components are bloated. You are also using components in your styleguide that aren't even using styled-system (Divider, ColorCircle, GridContainer, etc). Why recreate the typography for standard html elements. Why not just use styled-component's GlobalStyles to standardized them. I think styled-components alone would be more performant without styled-system unless there's some tree shaking that can be utilized.
npx create-react-app react-style guide --typescript
Should it be
react-style-guide
with two hyphens? As I find the command arguments not making sense to me. Thanks for the great article anyway.I was actually aiming for
npx create-react-app react-styleguide --typescript
but you can choose any name you want. Thank you for finding my mistake! Glad you enjoyed the read.TypeScript gives React too much boilerplate.
Debatable. For the features you get in return I think it's a fair tradeoff.
small price to pay for salvation
This would be much easier to follow along with if you specified which file each snippet should go.
One more thing I found. Your initial command to create the project,
npx create-react-app react-styleguide --typescript
isn't entirely correct and just creates a standard JS project. From create-react-app.dev/docs/adding-t... :Thank you for your suggestions man. I will update the article when I get the chance this weekend.
It looks like there's also a missing dev dependency:
@types/react-router-dom
. You have it in your repo'spackage.json
, but it's not listed above as a dependency to install. It is needed forLinkProps
.Just wanted to take 2 minutes to say this was a cool article: very informative.