Building a React Component Library | Part 3

Written by _alanbsmith | Published 2017/06/04
Tech Story Tags: css | react | nodejs | open-source | javascript

TLDRvia the TL;DR App

a tutorial for publishing your own component library to npm

Introduction

The purpose of this series is to walk through creating a small component library so you can learn how to build your own.

This is Part 3 of this series. If you haven’t already read Part 1 or Part 2, I would recommend doing that first.

In this section we’ll discuss:

  • Building a Label element
  • Making our Button element more extensible
  • Developing consistent grid layouts

Adding a Label Element

Now that we have a good feel for our workflow and building an element, let’s add another element, Label.

$ touch lib/elements/Label.js

Great! Now let’s build it.

import PropTypes from 'prop-types';
import styled from 'styled-components';

import * as colors from '../styles/colors';


const Label = styled.label`
  color: ${({ color }) => colors[color]};
  display: inline-block;
  font-size: 14px;
  font-weight: ${({ fontWeight }) => fontWeight};
  margin: 8px 0;
  text-transform: ${({ textTransform }) => textTransform};
  transition: all 300ms ease;
`;

Label.propTypes = {
  color: PropTypes.string,
  fontWeight: PropTypes.number,
  textTransform: PropTypes.string,
};

Label.defaultProps = {
  color: 'silver',
  fontWeight: 400,
  textTransform: 'uppercase',
};

export default Label;

This might look like a lot of code, but most of it should be familiar from Button. We have a few props our users can pass in: color, font-weight, and text-transform. And each of those have a default set. Now we’ll need to add it to lib/index.js so the build directory knows how to find it.

import Button from './elements/Button';
import Label from './elements/Label';

module.exports = {
  Button,
  Label,
};

Cool. So now lets experiment with it in our playground app!

NOTE: Make sure npm run build:watch and npm run lint:watch are running in your library and npm run dev is running in your app.

...
import { Button, Label } from 'component-lib'
...

const App = ({ name }) => {
  return (
    <div>
      <Label>Hello, {name}!</Label>
      <Button bgColor="orange">
        Click Me!
      </Button>
    </div>
  );
};
...

If everything went well, you should see this in your browser:

Not much to look at, I know, but it does confirm that our defaults are being applied properly. Feel free to play around and experiment with the props if you want. It’s okay, I’ll wait. 😀

At this point you might have some questions about why we’ve left font-size, line-height, and padding static on these elements. The answer is that I’ve been waiting for just such a segue into the topic. You’re absolutely right, all of these things should be dynamic. But we would like to set good guidelines for users to follow. So instead of letting them throw any font-size they want into the component, we’ll divide things into reasonable sizes. In the case of Labels, we’ll have small, medium, and large, which users can choose with a prop. To accomplish this we’ll add this to Label:

...
const labelSizes = {
  small: {
    'font-size': '12px',
    'line-height': '12px',
  },
  medium: {
    'font-size': '14px',
    'line-height': '16px',
  },
  large: {
    'font-size': '16px',
    'line-height': '16px',
  },
};

const Label = styled.label`
  color: ${({ color }) => colors[color]};
  display: inline-block;
  font-size: ${({ size }) => labelSizes[size]['font-size']};
  line-height: ${({ size }) => labelSizes[size]['line-height']};
  ...

Label.defaultProps = {
  color: 'silver',
  fontWeight: 400,
  size: 'medium',
  textTransform: 'uppercase',
};
...

Wait, how did you come up with those font sizes? Are they random?

Good question. Actually they are not random. A careful eye might have noticed that we’ve been using an eight-point grid system, and that’s mostly true. There’s a bit of math that goes into creating your own components, and it can be difficult to keep everything consistently spaced. A really nice way to accomplish this is to choose a grid system. We’re using an eight-point system, and all that means is that things are spaced and sized in multiples of 8px. It really doesn’t matter what you choose. I’ve seen 8, 10, and 12pt systems and they all work great. Their main purpose is to keep you from spacing one component 30px and another 32 0r 36px. It may not seem like a big deal, but those little inconsistent bits start to add up. Following the eight-point grid means we also keep font-sizes and line-heights in multiples of eight as well. So our “medium” baseline font should be 16px.

NOTE: If you’d like to read more on the eight-point grid system, this is a great article. And if you’d like to learn more about building consistent grid systems, this article is awesome.

Ok, got it. But why is your medium labelSize at 14px and not 16px?

Another great question. Visually, labels should be indicators and descriptors, but they should not dominate our attention. In fact, the best labels are the ones we forget are there after reading them. All that to say, if our medium font is 16px, our label should be just a bit under that, 2px under in this case, to help visually demote them. We’re also using the silver color to accomplish the same goal. Notice that we’re modifying line-height as well to be consistent with our eight-point grid system.

Now that we’ve got Label set up, let’s add some dynamic sizes to Button as well.

Adding Button Sizes

Buttons are a bit messier with sizing as there are so many different varieties. We want to accommodate our users needs, but we also need to create some consistency in our sizing. To do that we’ll add the following to Button:

...
const buttonSizes = {
  small: {
    'font-size': '14px',
    'line-height': '30px',
    padding: '0 8px',
  },
  medium: {
    'font-size': '16px',
    'line-height': '40px',
    padding: '0 12px',
  },
  large: {
    'font-size': '18px',
    'line-height': '50px',
    padding: '0 16px',
  },
  wide: {
    'font-size': '16px',
    'line-height': '40px',
    padding: '0 36px',
  },
  extraWide: {
    'font-size': '16px',
    'line-height': '40px',
    padding: '0 72px',
  },
  fullWidth: {
    'font-size': '16px',
    'line-height': '40px',
    padding: '0 8px',
  },
};

function setDisplay({ size }) {
  return size === 'fullWidth' ? 'block' : 'inline-block';
}

function setWidth({ size }) {
  return size === 'fullWidth' ? '100%' : 'initial';
}

const Button = styled.button`
  ...
  display: ${setDisplay};
  font-size: ${({ size }) => buttonSizes[size]['font-size']};
  line-height: ${({ size }) => buttonSizes[size]['line-height']};
  ...
  padding: ${({ size }) => buttonSizes[size]['padding']};
  ...
  width: ${setWidth};
  ...
`;

Button.defaultProps = {
  bgColor: 'blue',
  fontColor: 'white',
  size: 'medium',
};

...

We’ve added several sizes: small, medium, large, wide, extraWide, and fullWidth. We also added a couple functions setDisplay and setWidth to handle those attributes as well. These were extracted to functions mostly to make the syntax more readable than doing an inline ternary in the template literal.

Math Behind the Madness

I’ll talk very briefly about the relationship between the font-size, line-height, and padding here. Remember when I said we were “mostly following” an eight-point system. Well, this is the one occasion where I break it. Padding should be consistent across every side, and to accomplish this, we’re following the pattern:

line-height = font-size + 2(padding)

It works out like this:

// small
line-height = 14 + 8(2) // (30px)

// medium
line-height = 16 + 12(2) // (40px)

// large
line-height = 16 + 18(2) // (50px)

wide, extraWide, and fullWidth break this rule because we need wider buttons that don’t conform to this pattern, but do follow the eight-point grid system. Like I said before, buttons are messier. But the good news is that it’s your lib! And you can make sense of all this however you want. This is what I’ve landed on (for now), but you could do something totally different and that’s great!

Testing out Button Sizes

Okay, now that you understand some of the method behind the sizing, let’s test this out in the playground app. We’ll add the different button sizes so you can see the variations. We’re wrapping the buttons in divs to put each one on a new line.

...
const App = ({ name }) => {
  return (
    <div>
      <Label size="small">Hello, {name}!</Label>
      <div>
        <Button
          bgColor="green"
          size="small"
        >
          small
        </Button>
      </div>
      <div>
        <Button
          bgColor="yellow"
          size="medium"
        >
          medium
        </Button>
      </div>
      <div>
        <Button
          bgColor="orange"
          size="large"
        >
          large
        </Button>
      </div>
      <div>
        <Button
          size="wide"
        >
          wide
        </Button>
      </div>
      <div>
        <Button
          bgColor="pink"
          size="extraWide"
        >
          extra wide
        </Button>
      </div>
      <div>
        <Button
          bgColor="purple"
          size="fullWidth"
        >
          full width
        </Button>
      </div>
    </div>
  );
};
...

If everything went well, you should see something like this:

Various button sizes and colors

Wrapping Up

Awesome! Our buttons are way more useful with this consistent sizing. And we have a simple API for our users to follow. They can quickly build a button without having to think about what’s going on under the hood. We also added a Label element which will be useful when we build our component at the end. In Part 4, we’ll add our third and final element: a TextField!

NOTE: This would be a great place to save your work and commit.

$ git status
$ git add -A
$ git commit -m 'Adds Label element and Button sizes'

I hope this was helpful. If you enjoyed reading, let me know! And if you think it would end helpful for others, feel free to share!

Part 4 is currently being written and should be out soon! Thanks for your patience!


Published by HackerNoon on 2017/06/04