Credits: This is the code along of course Build a Complete Company Design System, but instead of cloning the repo I build it from scratch to use
latest dependencies instead of the fixed version used by author and a better understanding of the architecture.
- React
- TypeScript
- Yarn workspaces
- Style Dictionary
- TailwindCSS
- React Testing Library
- styled-components
- Axe
- Storybook
- GitHub Actions
3 packages:
- Foundation: host and distribute our design tokens and assets
- React: demo components Button, IconButton flexible enough and will be styled with our generated design-system styles tokens
- Storybook: lay out the best practices for developing/documenting components with Typescript
The flow goes from a design document figma, sketch, Adobe xd(source of truth) then extract design tokens
into a platform agnotic json
files
foundation/
├─ src/
│ ├─ tokens/
│ │ ├─ animations.json
│ │ ├─ color.json
│ │ ├─ radius.json
│ │ ├─ shadows.json
│ │ ├─ spacings.json
Then use style-dictionary cli
to create platform specific assets: scss/css var js-style-objects, for doing that a
style-dictionary config file
style dictionary config file(sd.config.js)
module.exports = { source: ['src/tokens/**/*.json'], platforms: { scss: { transformGroup: 'scss', buildPath: 'lib/tokens/scss/', files: [ { destination: 'tokens.scss', format: 'scss/variables', }, ], }, css: { transformGroup: 'css', buildPath: 'lib/tokens/css/', files: [ { destination: 'tokens.css', format: 'css/variables', }, ], }, 'js-src': { transformGroup: 'js', buildPath: 'src/tokens/js/', files: [ { name: 'tokens', destination: 'tokens.js', format: 'javascript/module', }, ], }, js: { transformGroup: 'js', buildPath: 'lib/tokens/js/', files: [ { name: 'tokens', destination: 'tokens.js', format: 'javascript/module', }, ], }, }, };
To generate assets, first define a package script
"scripts": {
"build-tokens": "style-dictionary build --config sd.config.js",
"build": "yarn build-tokens && tsc",
"build-tsc": "tsc --skipLibCheck"
},
Having that just run command:
$ yarn workspace @renato1010/foundation build
👆️ That command will generate assets in ./lib
folder
Finally, to facilitate the consumption of the assets, we need to make some changes to package.json and src/index.ts
package.json entry point and types
{ "name": "@renato1010/foundation", "packageManager": "yarn@3.3.1", "main": "./lib/index.js", "types": "./lib/index.d.ts",
index.ts: entry point
import tokens from './tokens/js/tokens';export { tokens };
Having tokens
as export from foundation
package, it's really easy to style our React components
import { forwardRef } from 'react';
import styled from 'styled-components';
import { tokens } from '@renato1010/foundation';
type ButtonProps = JSX.IntrinsicElements['button'] & {
/** Color based on the color props */
color: keyof typeof tokens.colors;
/** if button is in disabled state */
disabled?: boolean;
/** loading state */
loading?: boolean;
};
const ButtonStyled = styled.button<ButtonProps>`
/* Static styles */
all: unset;
cursor: pointer;
padding: 8px 20px;
&:disabled {
opacity: 40%;
}
/* Inherit from design tokens */
transition: ${tokens.animations.default.value};
color: ${tokens.colors.neutral.white.value};
border-radius: ${tokens.radius.large.value};
background-color: ${(props) => tokens.colors[props.color][500].value};
&:hover {
background-color: ${(props) => tokens.colors[props.color][700].value};
}
&:active {
background-color: ${(props) => tokens.colors[props.color][800].value};
}
`;
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ disabled, loading, color = 'primary', ...rest }, ref) => {
return <ButtonStyled {...rest} ref={ref} color={color} disabled={disabled || loading} />;
}
);
Create a story to visualize component variations and verify appearance and behavior Button.stories.tsx
import { Button } from '@renato1010/react/src/Button';
import { ComponentMeta, ComponentStory } from '@storybook/react';
export default {
title: 'renato1010/Button',
component: Button,
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const Default = Template.bind({});
Default.args = {
children: 'Default text',
};
run command:
$ yarn workspace @renato1010/storybook storybook
Got this: 👇️
Throughout the project, a11and standards were applied to ensure compliance.
Linting plugin .eslintrc.js
module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:prettier/recommended', ],
With Storybook: storybook config file
module.exports = { stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ '@storybook/addon-a11y', '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', ], framework: '@storybook/react', docs: { autodocs: true, }, core: { builder: 'webpack5', }, };
Testing with with Jest, custom matcher for a11y use lib: jest-axe
IconButton test for accessibility(a11y)
import React from 'react'; import { IconButton } from '../src/IconButton'; import { render, fireEvent, screen } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; import '@testing-library/jest-dom';expect.extend(toHaveNoViolations);
test('tests icon button render and click callback', async () => { const handleClick = jest.fn();
const { container } = render( <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" style={{ width: '1em', height: '1em' }} > ); const results = await axe(container); expect(results).toHaveNoViolations();
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1); });
*The steps related to package deployment were not taken into account because it was not my purpose