Making your React component library meaningful in 2021
Alexander Vechy
Posted on September 13, 2020
In the last article we managed to setup our project:
- use dokz as a documentation engine
- add stitches as a class names generator and manager of class names on components
Now, we're going to use:
-
typescript
to leverage type-safe tokens and props for our component library -
@react-aria
to make our components accessible
TypeScript
I'm not going to talk about the benefits of using TypeScript in this article, but I would say that [unfortunately], when your library is super awesome already, this is the only way forward to make it even more enjoyable. And we know our library is going to be the best, so we can start with TypeScript straight away:
yarn add --dev typescript @types/react
And create a tsconfig.json
(most of the stuff is added by next
, the base config was copied from here)
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"types": ["react"],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "lib", "pages"],
"exclude": ["node_modules"]
}
Now we rename our lib/*.js
files to .ts(x)
and we're done with the migration. We can now check that we get autocomplete suggesting possible values that we defined in our stitches.config.ts
:
Accessibility
Building accessible application is as important as having an elevator in the 9 stories building. You can skip building it, but you will hate yourself, people will hate you, and adding it to the existing building is... Well, at the very least is just expensive.
If you want to get familiar with the topic and don't really like to read specifications, I encourage you to read Accessible to all on web.dev.
But if you were ever wondering "Why do we need to do it ourselves? Why it's not built into the platform, if the standards are defined?", well, let's discuss it in the comments, I think we're just not there yet. I hope we will get some new API similar to how Date
gets improved or how Intl gets new features.
Before the future comes, we can use a "library of React Hooks that provides accessible UI primitives for your design system" from react-aria. This is the simplest way you can take to make your components accessible while keeping your business satisfied with the speed of delivery.
Let's start from the Button
component. First, we add simple lib/Button.tsx
:
import React from 'react';
import { styled } from '../stitches.config';
const ButtonRoot = styled('button', {
py: '$2',
px: '$3',
bc: '$blue500',
color: 'white',
fontSize: '14px',
br: '$sm',
});
const Button: React.FC = ({ children }) => {
return <ButtonRoot>{children}</ButtonRoot>;
};
export default Button;
One of the downsides of CSS-in-JS solutions is that you have to come up with even more variable names, like ugly ButtonRoot
Let's now create a playground for our Button
to see it in action. Create pages/components/Button.mdx
and add simple playground code:
---
name: Button
---
import { Playground } from 'dokz';
import Box from '../../lib/Box';
import Button from '../../lib/Button';
# Button
<Playground>
<Box css={{ p: '$8' }}>
<Button>Hello</Button>
</Box>
</Playground>
Box is just for offset for now
So here's what we have:
Now let's add our first react-aria
package:
yarn add @react-aria/button
And use it in our lib/Button.tsx
:
import React, { useRef } from 'react';
import { useButton } from '@react-aria/button';
import { styled } from '../stitches.config';
const ButtonRoot = styled('button', {
py: '$2',
px: '$3',
bc: '$blue600',
color: 'white',
fontSize: '14px',
br: '$sm',
});
const Button: React.FC = (props) => {
const ref = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(props, ref);
const { children } = props;
return (
<ButtonRoot {...buttonProps} ref={ref}>
{children}
</ButtonRoot>
);
};
export default Button;
Here, I'm just following the official instructions and I always encourage people to go to the docs directly and copy code from there than from the article. Just remember that code in the Internet isn't 100% valid unless taken from the official docs (then it's a least 90% valid)
Okay, this looks simple. What we've achieved? Actually, a lot. I'm pretty sure it's hard to buy the benefits when you don't know the context. So, if you're interested why you need all this code, why we need to handle "press management" on the button, I would suggest to read more in-depth articles by the author of the react-aria
: Building a Button.
Now, let's try this in the playground:
<Button onPress={() => alert('Wow')}>Make Wow</Button>
Now let's make sense out of our CSS-in-JS solution and create few button variants. I will use Tailwind CSS as a reference:
const ButtonRoot = styled('button', {
py: '$2',
px: '$3',
color: 'white',
fontSize: '14px',
fontWeight: 'bold',
transition: '0.2s ease-in-out',
variants: {
variant: {
default: {
'bc': '$blue500',
'color': 'white',
'br': '$md',
'&:hover': {
bc: '$blue700',
},
},
pill: {
'bc': '$blue500',
'color': 'white',
'br': '$pill',
'&:hover': {
bc: '$blue700',
},
},
outline: {
'bc': 'transparent',
'color': '$blue500',
'border': '1px solid $blue500',
'br': '$md',
'&:hover': {
bc: '$blue700',
borderColor: 'transparent',
color: 'white',
},
},
},
},
});
This will create a mapping between prop variant
and set of class names to be assigned to the button
component. You may notice that some styles are repeated between variants
. This is where I would strongly suggest to block any thoughts about extracting common styles into separate variables to make code DRY. Allow variant
s to be isolated unless you feel the need to extract something.
Now, when we have our variants defined, how do we use in our Button
component? Well, with some tricks:
const ButtonRoot = styled('button', {
/* common styles */
variants: {
variant: {
default: { /* styles */ },
pill: { /* styles */ },
outline: { /* styles */ },
},
},
});
type Props = React.ComponentProps<typeof ButtonRoot>;
const Button: React.FC<Props> = ({ as, variant = 'default', ...props }) => {
const ref = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(props as any, ref);
return (
<ButtonRoot {...buttonProps} variant={variant} as={as} ref={ref}>
{props.children}
</ButtonRoot>
);
};
First, we infer types generated by stitches
:
type Props = React.ComponentProps<typeof ButtonRoot>;
This allows us to get access to defined variant
prop and as
prop that stitches
provides to overwrite the HTML Element to render (BTW, you may argue whether you want this prop to be available for Button
or it's better to create a new component to handle specific case, for example for <a>
HTML element that looks like a button).
Second, we use this type for our Button
, for the consumers of this component to see what props are available, specifically what variant
one can apply:
const Button: React.FC<Props> = ({ as, variant = 'default', ...props }) => {
We also extract props that aren't default to <button>
element, just to make things clear. variant
prop gets default
variant (you can use Button.defaultProps
also for that).
Then, we shamelessly use any
:
const { buttonProps } = useButton(props as any, ref);
It's not the first and not the last time we have to use it. But when you deal with types that aren't first-class citizens in the language, chances are that even describing same thing can be done in different way. In this case, onFocus
prop expected by useButton
doesn't match with onFocus
prop that stitches
has in its type definitions for button
. But since we know it's <button>
and we expect people to pass only button
props – we can allow ourselves to use any
this time.
Let's see these variants in pages/components/Button.mdx
:
---
name: Button
---
import { Playground } from 'dokz';
import { Box, Button } from '../../build';
# Button
<Playground>
<Box css={{ p: '$8', display: 'flex', gap: '$3' }}>
<Button onPress={() => alert('Wow')}>Make Wow</Button>
<Button variant="pill" onPress={() => alert('Wow')}>
Make Wow
</Button>
<Button variant="outline" onPress={() => alert('Wow')}>
Make Wow
</Button>
</Box>
</Playground>
Saving, waiting a moment and...
Here we go!
If you want to test props autocomplete (unfortunately mdx
is not supported yet), try to write simple component even inside lib/Button.tsx
that uses this Button
component. You'll see inferred possible variant
s you can pass to the component:
So now we used some benefits of stitches
and react-aria
packages. I encourage you to check out more react-aria packages and see what else you can do with stitches, for example, how you can easily change the layouts based on the window
size using responsive styles.
Next, we are going to build and deploy the docs and the library, so that our foundation for component library is complete and we can start building more components.
Posted on September 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.