Next.js Component Library using typescript, twin.macro and Emotion with Yarn 2's Plug'n'Play working and Storybook to ease component development.
My personal components library using twin.macro and emotion-js as its CSS-in-JS solution. This repository uses Next.js however the components are reusable in any project that uses React, twin.macro and emotion-js (see https://github.com/ben-rogerson/twin.examples).
- For full details on the components, types and so on, see the typedoc documentation: https://interpause.github.io/interpause-components
- For interacting with the components live, see the Storybook.js documentation: https://storybook.interpause.dev
- To return back to the repository: https://github.com/Interpause/interpause-components
yarn install
# see it works by opening http://localhost:3000
yarn dev
# start the storybook (incomplete)
yarn storybook
To ensure Typescript linting works properly with Yarn 2 run these:
yarn add --dev @yarnpkg/pnpify
# for VSCode:
yarn pnpify --sdk vscode
See https://yarnpkg.com/getting-started/editor-sdks for other IDEs.
While I have yet to make it installable as a module, one possible approach for now is to add this repository as a remote:
git remote add components https://github.com/Interpause/interpause-components.git
git fetch components
# make sure you push to correct repository
git push --set-upstream origin main
I created a simple theming system via CSS variables. The 8 different accents are dynamically generated inside tailwind.config.js
:
primary
: emphasis, importantsecondary
: contrasting primaryinfo
: notifications, loading alerts, updatestrivial
: disabled, unimportant, extraneousgood
: success, logged in, purchases, loading completerisky
: warnings, confirmationsbad
: errors, wrong password, serious warningsnormal
: text
The base theme is included via:
//add this to pages/_app.tsx and .storybook/preview.js
import { Global } from '@emotion/react'
import { baseStyle } from '../src/theme/baseTheme'
<Global styles={baseStyle}/>
I preserved the --tw-opacity
CSS variables, allowing control over the intensity of the color via changing its opacity for various stuff such as backgrounds and borders:
{
"primary":"rgba(var(--hi-color-primary), var(--tw-text-opacity))"
}
// e.g. this is still possible
<div tw="bg-primary bg-opacity-50"></div>
Unfortunately, the above is currently backfiring for anything not on a plain background. For such components, I have made their backgrounds plain. TODO: A future approach might be to use the CSS3 hsla()
function and generate a bunch of tailwind classes for luminosity instead of using opacity as the way to control color intensity. Actually see tailwindlabs/tailwindcss#3850, might aid you in doing so.
As for how the base theme is configured by default, baseTheme.ts
:
/** Used to convert hex to `${r},${g},${b}`. */
const rgb = (c: string) => Color(c).array().join(',');
/** SerializedStyles containing default values for CSS vars. */
const themeVars = css`
--hi-color-primary: ${rgb('#0288d1')};
--hi-color-secondary: ${rgb('#311b92')};
--hi-color-info: ${rgb('#0288d1')};
--hi-color-trivial: ${rgb('#9e9e9e')};
--hi-color-good: ${rgb('#4caf50')};
--hi-color-risky: ${rgb('#fbc02d')};
--hi-color-bad: ${rgb('#d50000')};
--hi-color-normal: ${rgb('#000')};
--tw-text-opacity: 1;
--tw-placeholder-opacity: 0.65;
--tw-bg-opacity: 0.3;
--tw-border-opacity: 1;
--tw-divide-opacity: 0.2;
--tw-ring-opacity: 0.2;
`;
I have also made dark and light themes (really go check out baseTheme.ts
). Do follow it if you want to change the theme colors. As for changing the accent names and so on, my code in tailwind.config.js
should be fairly easy to change.
Finally, I provided a function in baseTheme.ts
to make it easy to change the accent of a component easily:
/** creates a SerializedStyles that sets all colors to that of the accent given. */
const getAccent = (accent:accentTypes) => css`
color: rgba(var(--hi-color-${accent}), var(--tw-text-opacity));
background-color: rgba(var(--hi-color-${accent}), var(--tw-bg-opacity));
border-color: rgba(var(--hi-color-${accent}), var(--tw-border-opacity));
--tw-ring-color: rgba(var(--hi-color-${accent}), var(--tw-ring-opacity));
--tw-ring-offset-color: rgba(var(--hi-color-${accent}), 1);
& > * + * {
border-color: rgba(var(--hi-color-${accent}), var(--tw-divide-opacity));
}
&::placeholder {
color: rgba(var(--hi-color-${accent}), var(--tw-placeholder-opacity));
}
`;
You can follow along with the commit history of this repository to see the effects of each step.
- Setup Next.js with Typescript
- (Optional) Setup Yarn 2 PnPify
- Setup twin.macro and emotion
- Setup Storybook
First, create the Next.js project:
yarn create next-app
Next, create tsconfig.json
in the root folder and run:
yarn add --dev typescript @types/react @types/node
# Next.js initializes tsconfig.json for you on the first run
yarn dev
Optionally, change these tsconfig.json
settings once done:
{
"allowJs": false,
"strict": true
}
As we will not be using them anymore, you may delete ./styles
. Look in ./pages
for how code can be written once installation is complete. If using this repository as a Next.js template, see https://nextjs.org/docs/basic-features/typescript for further details.
To setup Yarn 2:
yarn set version berry
Add these to the generated .gitignore
:
# dependencies
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/versions
Then do:
yarn add --dev @yarnpkg/pnpify
# for VSCode:
yarn pnpify --sdk vscode
# see https://yarnpkg.com/getting-started/editor-sdks for other IDEs
# you might want to add to .gitignore some of the generated files like .vscode
If you run into module resolution problems, you can try adding to .yarnrc.yml
:
nodeLinker: "pnp"
pnpMode: "loose"
Adapted from https://github.com/ben-rogerson/twin.examples/tree/master/next-emotion. My steps are very similar to that of the original, but additional dependencies @emotion/babel-plugin babel-plugin-macros
are needed if you are using Yarn 2. Do take a look at the original as it covers some of the features as well as contains optional steps that I skipped. Perhaps this is specifically an issue with VSCode, but I had to use "reload window" sometimes to get the Typescript linter to update, so try that if you get weird warnings.
First, install the dependencies:
yarn add twin.macro tailwindcss @emotion/react @emotion/styled @emotion/css
yarn add --dev @emotion/babel-plugin babel-plugin-macros
In _app.tsx add <GlobalStyles/>
like this:
import { GlobalStyles } from 'twin.macro';
export default function App({Component, pageProps}:AppProps){
return <>
<GlobalStyles/>
<Component {...pageProps}/>
</>;
}
Create .babelrc.js
in the root folder and add:
module.exports = {
presets: [
[
'next/babel',
{
'preset-react': {
runtime: 'automatic',
importSource: '@emotion/react',
},
},
],
],
plugins: ['@emotion/babel-plugin', 'babel-plugin-macros'],
}
Then, create next.config.js
in the root folder and add:
module.exports = {
webpack: (config, { isServer }) => {
// Fixes packages that depend on fs/module module
if (!isServer) {
config.node = { fs: 'empty', module: 'empty' }
}
return config
},
}
Finally, create twin.d.ts
in the root folder and add these type declarations:
import 'twin.macro'
import styledImport from '@emotion/styled'
import { css as cssImport } from '@emotion/react'
// The css prop
// https://emotion.sh/docs/typescript#css-prop
import {} from '@emotion/react/types/css-prop'
declare module 'twin.macro' {
// The styled and css imports
const styled: typeof styledImport
const css: typeof cssImport
}
// The 'as' prop on styled components
declare global {
namespace JSX {
interface IntrinsicAttributes<T> extends DOMAttributes<T> {
as?: string
}
}
}
And include it into tsconfig.json
:
{
"include": ["twin.d.ts"]
}
See ./pages/index.tsx
for code that uses twin.macro's features to see if everything so far is setup correctly.
Get it from https://marketplace.visualstudio.com/items?itemName=lightyen.tailwindcss-intellisense-twin. Great extension that is better than the official one specifically for twin.macro.
To add Storybook.js:
#Storybook v6.2.0 was needed to solve something related to core-js
yarn add --dev @storybook/cli@next prop-types @emotion/babel-plugin-jsx-pragmatic @babel/plugin-transform-react-jsx
yarn sb init
yarn storybook
Create ./.storybook/.babelrc
and add:
{
"presets": [
[ "next/babel" ]
],
"plugins": [
"babel-plugin-macros",
[
"@emotion/babel-plugin-jsx-pragmatic",
{
"export": "jsx",
"import": "__cssprop",
"module": "@emotion/react"
}
],
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "__cssprop"
},
"emotion-css-prop"
]
]
}
Adapted from https://github.com/ben-rogerson/twin.examples/blob/master/storybook-emotion/.storybook/.babelrc. A different set of plugins had to be used for Storybook's .babelrc
to work. This is probably because the way Next.js and Storybook transpiles is different, leading to the @emotion/babel-plugin
not working for storybook. Presets had to be respecified too as they were overwritten. Also, see ./src/containers/Card.stories.js
for an example.
Finally, to ./.storybook/preview.js
add:
import { GlobalStyles } from 'twin.macro'
export const decorators = [
Story => (
<div>
{/* */}
<GlobalStyles />
<Story />
</div>
),
]
Many thanks to ben-rogerson for developing twin.macro, if not for which none of this would be possible. I really like the twin.macro + emotionjs library to the point when I tried to switch to a component library, I was actually put off by the relative difficulty of styling things. He had also made several examples of how to use twin.macro with various frameworks, without which it would have taken me much longer to get this to work.
Most of my components will have a type
and variant
prop. type
refers to mainly the accent, allowing you to customize which accent is used for the component. variant
refers to the style of the component, for example, an outlined button with transparent background versus one that is filled in.
All components will pass the className
prop to the root element, allowing you to style them directly using the tw
or css
props. Components that contain other sub-components that makes sense to be stylable will attach classes to those sub-components so that they can be styled from outside. In that case, the classes will be mentioned in the documentation. Else, you could use the browser's debtools to inspect the classes added. Refs are sometimes forwarded.
Note to self, bookmark this: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts. Still hate how tslinter seems to arbitrarily resolve or not resolve types. Really wish they would show Omit<...> rather than resolve it automatically to Pick<...super long list...>.