This is the first part of a new series I’m starting where we create our own component library from scratch and that we’ll eventually release on NPM! This part will cover setting up and creating a simple Button component with a couple of twists.
Component libraries have never been more popular. They provide a lot of value for both companies and engineers. Consistent and beautiful design, pre-built components to save development time, extended functionality and the list goes on if we start to collect the benefits of using one.
This is the stack I choose for this component library:
- TypeScript
- Tailwind
- Storybook
Installing dependencies
Not so long ago I found TSDX that takes care of all the setting up instead of us having to do it from scratch. It even comes with Storybook installed and configured out of the box! Let’s create a app using the TSDX installed in our terminal:
npx tsdx create 'packagename'
When you get prompted to choose a template, pick react-with-storybook
After the installer finished, let’s enter our new project with cd 'packagename'
and let’s install the dependencies that we’ll need for Tailwind.
yarn add --dev tailwindcss postcss autoprefixer @storybook/addon-postcss rollup-plugin-postcss
Tailwind and PostCSS setup
Now that we have all the dependencies, let’s setup Tailwind and PostCSS as they don’t come with TSDX. First let’s initialize Tailwind with the PostCSS prefix with the following command:
npx tailwindcss init -p
This’ll generate postcss.config.js
and tailwind.config.js
. The only thing we need to change is in the tailwind.config.js
file. We’ll modify the content
property to ensure that Tailwind will apply the styles to our components. Here’s our file after the update:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
Next we’ll create a file in our package on the top level called tsdx.config.js
. This is a config file (similar to what Next.js or other frameworks use) that help you override/extend the default settings of your app. In here we’ll add the following snippet that’ll help integrate PostCSS with Rollup:
const postcss = require('rollup-plugin-postcss');
module.exports = {
rollup(config, options) {
config.plugins.push(
postcss({
config: {
path: './postcss.config.js',
},
extensions: ['.css'],
minimize: true,
inject: {
insertAt: 'top',
},
})
);
return config;
},
};
Storybook has an addon (that we installed earlier) that we need to include in the Storybook config so it’ll apply the styles in preview mode. To do so, we need to update the .storybook/main.js
file with the following snippet:
module.exports = {
stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
{
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
// <https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration>
typescript: {
check: true, // type-check stories during Storybook build
},
};
And with that out of the way, the only thing left to do is to create a CSS file in our src
folder, called tailwind.css
and set the following content:
@tailwind base;
@tailwind components;
@tailwind utilities;
Let’s import it in our src/index.tsx
file so our components will have the styles applied. We can even test it here if it works properly by setting a class on our example component:
import React, { FC, HTMLAttributes, ReactChild } from 'react';
import './tailwind.css';
export interface Props extends HTMLAttributes<HTMLDivElement> {
/** custom content, defaults to 'the snozzberries taste like snozzberries' */
children?: ReactChild;
}
// Please do not use types off of a default export module or else Storybook Docs will suffer.
// see: <https://github.com/storybookjs/storybook/issues/9556>
/**
* A custom Thing component. Neat!
*/
export const Thing: FC<Props> = ({ children }) => {
return <div className="bg-blue-400 ">{children || `the snozzberries taste like snozzberries`}</div>;
};
Let’s run yarn storybook
and if everything went well, you should see the text with a light blue background.
Creating our first component
Now it’s time to create our first component. For this article I decided to create a Button component. Let’s start by creating a new folder inside our src
folder and name it components
. Once that’s done, create Button/index.tsx
inside your components
folder.
Let’s take a step back before we continue and let’s define how we want our button to work. To keep it simple yet pretty versatile, I want to be able to change the size
, color
and variant
property on my button. Variant
in this case refers to an option between filled
or outlined
button.
- Various sizes —
sm
,md
orlg
. Defaults tomd
- Various colours —
primary
orsecondary
. Defaults toprimary
- Outline —
filled
oroutlined
. Defaults tofilled
- Handles click events — accepts an
onClick
method as a prop that it attaches to the button. - Can render text or other components inside — Will receive
children
prop.
With the planning out of the way, let’s write some code! First let’s create a new folder called components
and inside that a file called Button.tsx
. Luckily our component is really simple and straightforward, so here is it:
import React from 'react';
type ButtonVariant = 'filled' | 'outlined';
type ButtonColor = 'primary' | 'secondary';
type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
color?: ButtonColor;
size?: ButtonSize;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'filled',
color = 'primary',
size = 'md',
children,
...props
}) => {
const baseStyles =
'rounded font-semibold focus:outline-none focus:ring-2 focus:ring-offset-2';
const sizeStyles = {
sm: 'py-1 px-2 text-sm',
md: 'py-2 px-4',
lg: 'py-3 px-6 text-lg',
};
const colorStyles = {
primary: {
filled: 'bg-blue-500 hover:bg-blue-600 text-white focus:ring-blue-400',
outlined:
'bg-transparent hover:bg-blue-100 text-blue-500 border-blue-500 border focus:ring-blue-400',
},
secondary: {
filled: 'bg-gray-500 hover:bg-gray-600 text-white focus:ring-gray-400',
outlined:
'bg-transparent hover:bg-gray-100 text-gray-500 border-gray-500 border focus:ring-gray-400',
},
};
const buttonStyles = `${baseStyles} ${sizeStyles[size]} ${colorStyles[color][variant]}`;
return (
<button className={buttonStyles} {...props}>
{children}
</button>
);
};
To break it up a bit, you can see on the top we have our type definitions for our props. Below that is our actual component and a few lines of tailwind. Split up to baseStyles
, meaning those classes apply to all of our scenarios. After that comes sizeStyles
and colorStyles
that relies on the prop you pass in to assign the correct classes. Next is buttonStyles
which is just a collection of all the classes that’ll be applied on our button. It’s nicer to have it separately rather than piling it all up in our JSX. And lastly is our component which I really hope is clear.
Now that we have our component ready, let’s create some Storybook stories for it so we can preview it. Let’s create a new file inside our stories
folder, called Button.stories.tsx
. Inside this file we’ll have 3 views with all the variants of our button. One for colours, one for variants and finally a view for the different sizes compared to each other. Let’s add this to our newly created file:
import React from 'react';
import { Meta } from '@storybook/react/types-6-0';
import { Button, ButtonProps } from '../src/components/Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: { type: 'radio', options: ['filled', 'outlined'] },
},
color: {
control: { type: 'radio', options: ['primary', 'secondary'] },
},
size: {
control: { type: 'radio', options: ['sm', 'md', 'lg'] },
},
},
} as Meta;
export const Colors = (args: ButtonProps) => (
<div className="flex flex-col space-y-4 mt-8">
<div>
<Button {...args} color="primary">
Outlined
</Button>
</div>
<div>
<Button {...args} color="secondary">
Filled
</Button>
</div>
</div>
);
Colors.args = {
variant: 'filled',
};
export const Variants = (args: ButtonProps) => (
<div className="flex flex-col space-y-4 mt-8">
<div>
<Button {...args} variant="outlined">
Outlined
</Button>
</div>
<div>
<Button {...args} variant="filled">
Filled
</Button>
</div>
</div>
);
Variants.args = {
color: 'primary',
};
export const Sizes = (args: ButtonProps) => (
<div className="flex flex-col space-y-4 mt-8">
<div>
<Button {...args} size="sm">
Small
</Button>
</div>
<div>
<Button {...args} size="md">
Medium
</Button>
</div>
<div>
<Button {...args} size="lg">
Large
</Button>
</div>
</div>
);
Sizes.args = {
variant: 'filled',
color: 'primary',
};
To preview these stories, all you need to do is run yarn storybook
and you’ll see your newly created button with all it’s different states displayed.
Now that we got our Button rendering, let’s remove some of the boilerplate code that we won’t be needing anymore. I’ll remove the example component from our src/index.tsx
file, and only contain the Tailwind import and to export our Button component. This is how it should look like:
import './tailwind.css';
export * from './components/Button';
You can also remove the Storybook file for the example component as we won’t need that anymore and that’s all! Now we have our Component Library containing our own Button component! Here is the Github repo for this article
I hope you enjoyed this article and let me know which component would you like to see in Part 2!