Building Component Library with Docz and Lerna

Updated on · 8 min read
Building Component Library with Docz and Lerna

Component libraries are all the rage these days, with many companies rolling out their own solutions or sticking to a bunch of open source alternatives. Leveraging a component library for UI development, particularly in large teams, has a lot of cool benefits. It allows to take full advantage of modular and reusable UI components, which brings increased speed of development and unifies styles across multiple teams and apps. Combine that with a robust design system, and the handover from design to development teams becomes smooth and more efficient.

Frameworks/libraries like React, Vue, etc are perfectly suited for this purpose since there are designed to be highly modular. In this post React and Styled components are used as main tools of choice for developing components.

There are also some helpful tools, that could be leveraged to speed up the development process and deployment of the library. Embracing the modular approach, it would make sense that each component would be an own npm package, the whole library being a monorepo. That's where Lerna will be used to manage multiple packages inside the project, as well as to keep track of their versioning and publishing process.

In order to test and document the components, Docz is used (as an alternative to Storybook). It allows documenting components with MDX, which is a format that combines JSX and Markdown, basically making it possible to import React components inside Markdown files. Moreover, Docz version 2 runs on GatsbyJS, which brings increased development and build speeds and enables access to Gatsby's vast network of plugins and tools.

Lerna setup

We'll start by creating a new project, titled uikit, and installing the required dependencies.

bash
$ npm i -g lerna $ mkdir uikit && cd $_ $ yarn add docz react react-dom styled-components
bash
$ npm i -g lerna $ mkdir uikit && cd $_ $ yarn add docz react react-dom styled-components

With the core dependencies installed, it's time to initialize the Lerna project.

bash
$ lerna init
bash
$ lerna init

This will create the following project structure:

shell
ui-kit/ packages/ package.json lerna.json
shell
ui-kit/ packages/ package.json lerna.json

The UI components will be stored in the packages folder.

Now let's examine the generated lerna.json, which serves as a configuration file for Lerna. By default, there isn't much going on, and after a few customizations the config will look as follows.

json
{ "npmClient": "yarn", "version": "independent", "packages": ["packages/*"], "useWorkspaces": true }
json
{ "npmClient": "yarn", "version": "independent", "packages": ["packages/*"], "useWorkspaces": true }

The most important changes here are selecting yarn as npm client, specifying independent versioning, so the package versions can be changed independently of each other, and enabling Yarn workspaces. The packages option points to the location of our library packages, for which we'll keep the default setting. The more extensive list of configuration options is available on Lerna's Github page.

Additionally, we'll need to add workspaces-related options to the root package.json.

json
{ "name": "uikit", "license": "MIT", "workspaces": { "packages": ["packages/*"] }, "private": true, "dependencies": { "docz": "^2.2.0", "lerna": "^3.20.2", "react": "^16.12.0", "react-dom": "^16.12.0", "styled-components": "^5.0.0" }, "devDependencies": { "prettier": "^1.19.1" } }
json
{ "name": "uikit", "license": "MIT", "workspaces": { "packages": ["packages/*"] }, "private": true, "dependencies": { "docz": "^2.2.0", "lerna": "^3.20.2", "react": "^16.12.0", "react-dom": "^16.12.0", "styled-components": "^5.0.0" }, "devDependencies": { "prettier": "^1.19.1" } }

Here we specify the path to workspaces, which is the same as the one in lerna.json. Also, we have to make the package private, otherwise workspaces won't work.

Creating the first component

To kick things off with the dev work, let's add the first package - Typography, with the necessary base font components. As a result, the project's structure will be updated as follows.

shell
ui-kit/ packages/ typography/ src/ index.js CHANGELOG.md package.json package.json lerna.json
shell
ui-kit/ packages/ typography/ src/ index.js CHANGELOG.md package.json package.json lerna.json

Before actually writing the font components, let's make a few modifications to the typography's package.json.

json
{ "name": "@uikit/typography", "version": "1.0.0", "description": "Base fonts", "main": "dist/index.js", "module": "src/index.js", "files": ["dist", "CHANGELOG.md"], "author": "", "license": "MIT" }
json
{ "name": "@uikit/typography", "version": "1.0.0", "description": "Base fonts", "main": "dist/index.js", "module": "src/index.js", "files": ["dist", "CHANGELOG.md"], "author": "", "license": "MIT" }

The most interesting here are main, module and files fields. We'll point main to the dist folder, where the transpiled files will be stored and later used in the installed package. The module will point to the src folder, so the packages can be imported directly from the source folder during development and the changes will be reflected immediately without needing to bootstrap packages again or run build script. Finally, the files property contains the list of the files, which will be included in the published package.

Now we can set up some basic font styles in typography's index.js. Those will be made as styled components.

javascript
// typography/src/index.js import styled, { css } from "styled-components"; const fontFamily = "sans-serif"; const fontWeights = { light: 300, regular: 400, bold: 600, }; const baseStyles = css` font-family ${fontFamily}; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; font-weight: ${({ fontWeight }) => fontWeights[fontWeight] || fontWeights.regular}; `; export const H1 = styled.h1` ${baseStyles}; font-size: 62px; letter-spacing: -3px; line-height: 62px; `; export const H2 = styled.h2` ${baseStyles}; font-size: 46px; letter-spacing: -3px; line-height: 46px; `; export const H3 = styled.h3` ${baseStyles}; font-size: 30px; letter-spacing: -2px; line-height: 30px; `; export const H4 = styled.h4` ${baseStyles}; font-size: 24px; letter-spacing: -1.5px; line-height: 24px; `; export const H5 = styled.h5` ${baseStyles}; font-size: 20px; letter-spacing: -1px; line-height: 20px; `; export const H6 = styled.h6` ${baseStyles}; font-size: 18px; letter-spacing: 0; line-height: 18px; `; export const Text = styled.p` ${baseStyles}; font-size: 16px; letter-spacing: 0; line-height: 16px; `; export const SmallText = styled.small` ${baseStyles}; font-size: 12px; letter-spacing: 0; line-height: 12px; `;
javascript
// typography/src/index.js import styled, { css } from "styled-components"; const fontFamily = "sans-serif"; const fontWeights = { light: 300, regular: 400, bold: 600, }; const baseStyles = css` font-family ${fontFamily}; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; font-weight: ${({ fontWeight }) => fontWeights[fontWeight] || fontWeights.regular}; `; export const H1 = styled.h1` ${baseStyles}; font-size: 62px; letter-spacing: -3px; line-height: 62px; `; export const H2 = styled.h2` ${baseStyles}; font-size: 46px; letter-spacing: -3px; line-height: 46px; `; export const H3 = styled.h3` ${baseStyles}; font-size: 30px; letter-spacing: -2px; line-height: 30px; `; export const H4 = styled.h4` ${baseStyles}; font-size: 24px; letter-spacing: -1.5px; line-height: 24px; `; export const H5 = styled.h5` ${baseStyles}; font-size: 20px; letter-spacing: -1px; line-height: 20px; `; export const H6 = styled.h6` ${baseStyles}; font-size: 18px; letter-spacing: 0; line-height: 18px; `; export const Text = styled.p` ${baseStyles}; font-size: 16px; letter-spacing: 0; line-height: 16px; `; export const SmallText = styled.small` ${baseStyles}; font-size: 12px; letter-spacing: 0; line-height: 12px; `;

Note that css helper from styled-components is used to define reusable parts of the styles, which are then extended by other components. The components also accept a fontWeight property for customization, which defaults to regular.

Trying out Docz playground

This seems like a good time to try these components out in action and that's where Docz will be used to document their usage. In order to do that, we'll need to add an .mdx file somewhere in the project with the component documentation, and one of those files needs to point to route: / and will be used as the front page. Let's create this index.mdx in the root of the packages.

markdown
// index.mdx --- name: Welcome route: / --- # Welcome to the awesome UI Kit Select any of the components from the sidenav to get started.
markdown
// index.mdx --- name: Welcome route: / --- # Welcome to the awesome UI Kit Select any of the components from the sidenav to get started.

After running yarn docz dev, we can navigate to localhost:3000 and see the front page of the library.

Front page

To add documentation to the typography, we'll create a docs folder inside the package and add typography.mdx there.

shell
ui-kit/ packages/ typography/ docs/ typography.mdx src/ index.js CHANGELOG.md package.json package.json lerna.json
shell
ui-kit/ packages/ typography/ docs/ typography.mdx src/ index.js CHANGELOG.md package.json package.json lerna.json

To document components, we'll use a special docz component, called Playground. Wrapping it around the components will allow editing them right below where they are displayed.

markdown
--- name: Typography menu: Components --- import { Playground } from 'docz'; import { H1, H2, H3, H4, H5, H6, Text, SmallText } from '../src/index'; # Base Typography <Playground> <H1>Heading 1</H1> <H2>Heading 2</H2> <H3>Heading 3</H3> <H4>Heading 4</H4> <H4 fontWeight='bold'>Heading 4 bold</H4> <H5>Heading 5</H5> <H6>Heading 6</H6> <Text>Text</Text> <SmallText>SmallText</SmallText> </Playground>
markdown
--- name: Typography menu: Components --- import { Playground } from 'docz'; import { H1, H2, H3, H4, H5, H6, Text, SmallText } from '../src/index'; # Base Typography <Playground> <H1>Heading 1</H1> <H2>Heading 2</H2> <H3>Heading 3</H3> <H4>Heading 4</H4> <H4 fontWeight='bold'>Heading 4 bold</H4> <H5>Heading 5</H5> <H6>Heading 6</H6> <Text>Text</Text> <SmallText>SmallText</SmallText> </Playground>

After refreshing the page, or restarting dev sever if necessary, we'd be able to see our typography components. And the best thing is that we can directly edit the code on the page and see the updated results immediately!

Updated results

Adding custom fonts

This works well for built-in fonts, but what if we want to load a custom font, say from Google fonts? Unfortunately, since v2 of Docz has been released quite recently and due to it being a major rewrite of v1, there's still no clear, documented way to do that. However, there's one solution, which also nicely demonstrates the extendability of Gatsby configuration and a concept, known as Component shadowing.

For Gatsby-specific components we'll need to create a src folder in the root of the project, where the theme-specific components, among others, will be stored. Since we're extending gatsby-theme-docz, a folder with this name needs to be created inside the src. Lastly, we'll create a wrapper.js file inside of it to have the following project structure.

shell
ui-kit/ packages/ typography/ docs/ typography.mdx src/ index.js CHANGELOG.md package.json src/ gatsby-theme-docz/ wrapper.js package.json lerna.json
shell
ui-kit/ packages/ typography/ docs/ typography.mdx src/ index.js CHANGELOG.md package.json src/ gatsby-theme-docz/ wrapper.js package.json lerna.json

Inside wrapper.js we'll add a very simple component, the only task of which is to pass down its children.

javascript
// src/gatsby-theme-docz/wrapper.js import React, { Fragment } from "react"; export default ({ children }) => <Fragment>{children}</Fragment>;
javascript
// src/gatsby-theme-docz/wrapper.js import React, { Fragment } from "react"; export default ({ children }) => <Fragment>{children}</Fragment>;

It seems quite pointless to make a component which only forwards the children, however the reason for this is that we can now include css styles in this component, which will be applied globally. For that, let's create styles.css alongside wrapper.js and import there one of the selected fonts. In this tutorial, we'll be using Montserrat.

css
/* src/gatsby-theme-docz/styles.css */ @import url("https://fonts.googleapis.com/css?family=Montserrat:300,400,600&display=swap");
css
/* src/gatsby-theme-docz/styles.css */ @import url("https://fonts.googleapis.com/css?family=Montserrat:300,400,600&display=swap");

Now we just need to import this file into wrapper.js and update the fontFamily constant for the typography.

javascript
// src/gatsby-theme-docz/wrapper.js import React, { Fragment } from "react"; import "./style.css"; export default ({ children }) => <Fragment>{children}</Fragment>;
javascript
// src/gatsby-theme-docz/wrapper.js import React, { Fragment } from "react"; import "./style.css"; export default ({ children }) => <Fragment>{children}</Fragment>;
javascript
// ./packages/typography/src/index.js import styled, { css } from "styled-components"; const fontFamily = "'Montserrat', sans-serif"; // ...
javascript
// ./packages/typography/src/index.js import styled, { css } from "styled-components"; const fontFamily = "'Montserrat', sans-serif"; // ...

The changes should be visible immediately (if not, might need to restart the dev server). This might not be the cleanest approach, but it gets the job done, and since it's no longer possible to load custom fonts via doczrc.js, this might be one of the few viable solutions.

Customizing the documentation site

Talking about doczrc.js, which is used to configure a Docz project. The list of configuration options can be found on the project's documentation site. Since we're now using Montserrat font for UI kit's typography, it would make sense if our documentation website used the same font. To do that, we'll add a themeConfig property to the doczrc.js, where the styles for the most commonly used text elements will be applied.

javascript
const fontFamily = "'Montserrat', sans-serif"; export default { title: "UI Kit", description: "UI Kit - Collection of UI components", themeConfig: { styles: { h1: { fontFamily: fontFamily, }, h2: { fontFamily: fontFamily, }, body: { fontFamily: fontFamily, }, }, }, };
javascript
const fontFamily = "'Montserrat', sans-serif"; export default { title: "UI Kit", description: "UI Kit - Collection of UI components", themeConfig: { styles: { h1: { fontFamily: fontFamily, }, h2: { fontFamily: fontFamily, }, body: { fontFamily: fontFamily, }, }, }, };

Since we need to keep our project configuration separate from the components, we'll have to declare the font family separately here and use it for specific text elements. Additionally, we can customize the project title and description here. The default themeConfig can be found on the Docz's Github page. More options to customize the project, like adding a custom logo, are described in the documentation.

Adding Buttons

Finally, it's time to add a React component, Buttons, which will also make use of the typography for better illustration of how components can be used together. As before, we'll make a new package, so the project's structure will be as follows.

shell
ui-kit/ packages/ typography/ docs/ typography.mdx src/ index.js CHANGELOG.md package.json buttons/ docs/ buttons.mdx src/ index.js Buttons.js CHANGELOG.md package.json src/ gatsby-theme-docz/ style.css wrapper.js package.json lerna.json
shell
ui-kit/ packages/ typography/ docs/ typography.mdx src/ index.js CHANGELOG.md package.json buttons/ docs/ buttons.mdx src/ index.js Buttons.js CHANGELOG.md package.json src/ gatsby-theme-docz/ style.css wrapper.js package.json lerna.json

The package.json for buttons will look almost identical to the one from typography, with a few small exceptions. The most notable one is that buttons has typography package as a dependency.

json
{ "name": "@uikit/buttons", "version": "1.0.0", "description": "Button components", "main": "dist/index.js", "module": "src/index.js", "files": ["dist", "CHANGELOG.md"], "dependencies": { "@uikit/typography": "^1.0.0" }, "author": "", "license": "MIT" }
json
{ "name": "@uikit/buttons", "version": "1.0.0", "description": "Button components", "main": "dist/index.js", "module": "src/index.js", "files": ["dist", "CHANGELOG.md"], "dependencies": { "@uikit/typography": "^1.0.0" }, "author": "", "license": "MIT" }

Now, after we run lerna bootstrap, it will install all the required packages and symlink the dependencies inside the packages folder. One nice benefit of this is that if we make any changes to the typography package and use that package inside buttons, the changes will be immediately reflected in both packages without needing to rebuild or publish any of them. This makes the development experience really fast and efficient!

After all the dependencies have been installed, we can start writing code for the buttons.

javascript
// packages/buttons/src/Buttons.js import React from "react"; import styled from "styled-components"; import { SmallText } from "@uikit/typography"; export const ButtonSmall = ({ text, ...props }) => { return ( <Button {...props}> <SmallText>{text}</SmallText> </Button> ); }; export const Button = styled.button` border-radius: 4px; padding: 8px 16px; color: white; background-color: dodgerblue; border-color: dodgerblue; `; // packages/src/buttons/index.js export * from "./Buttons";
javascript
// packages/buttons/src/Buttons.js import React from "react"; import styled from "styled-components"; import { SmallText } from "@uikit/typography"; export const ButtonSmall = ({ text, ...props }) => { return ( <Button {...props}> <SmallText>{text}</SmallText> </Button> ); }; export const Button = styled.button` border-radius: 4px; padding: 8px 16px; color: white; background-color: dodgerblue; border-color: dodgerblue; `; // packages/src/buttons/index.js export * from "./Buttons";

Here we define two very basic button components. The Button component has a few base styles, which could be further extended. ButtonSmall has a predefined text component and therefore accepts button text as a separate prop. Additionally, we export everything from Buttons.js inside index.js as a convenience. This will ensure a single point of export for each package, particularly helpful when there are multiple files per package. Now let's try these new components out in the playground.

markdown
// packages/buttons/docs/buttons.mdx --- name: Buttons menu: Components --- import { Playground } from 'docz'; import { Button, ButtonSmall } from '../src/index'; # Buttons ## Base button <Playground> <Button>Test</Button> </Playground> ## Small button <Playground> <ButtonSmall text='Click me'/> </Playground>
markdown
// packages/buttons/docs/buttons.mdx --- name: Buttons menu: Components --- import { Playground } from 'docz'; import { Button, ButtonSmall } from '../src/index'; # Buttons ## Base button <Playground> <Button>Test</Button> </Playground> ## Small button <Playground> <ButtonSmall text='Click me'/> </Playground>

Navigating back to localhost:3000 we can confirm that the buttons work as expected. With that we have a properly documented, functioning component library, which can be easily extended.

Deploying the docs and publishing packages

So far we have been focusing mostly on development side of the component library, however there are a few other important steps that need to happen before the library becomes usable.

Publishing packages

To publish all the packages that have been changed since the last publishing took place (and after they have been transpiled with Babel), we can use lerna publish command. It will prompt to specify versioning for each package before publishing them. The version can be specified directly with the publish command, which will apply the same versioning to all the changed packages and will skip the prompts, e.g. lerna publish minor. For publishing to work, a registry needs to be added in lerna.json.

json
"command": { "publish": { "registry": "https://mypackageregistry/" } }
json
"command": { "publish": { "registry": "https://mypackageregistry/" } }

Building and serving docs

Docz comes with a few built-in scripts that make it easier to view and deploy the documentation. It can be built and served locally by running yarn docs build && yarn docz serve. To deploy the documentation online Docz's site has a handy example of doing it with Netlify. After Netlify site has been set up, deploying is easy via running netlify deploy --dir .docz/dist.

If you want to have a look at the boilerplate code for the component library, it's available on Github.