/build-design-system-monorepo

Create a design system from a figma file, then extract design tokens and generate platform-specific assets using the style-dictionary cli tool. Consume style assets in React components. Use storybook to render React components and confirm that the style works as expected.

Primary LanguageJavaScript

Build complete company design system

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.

Tech Stack:

Monorepo

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

Take away from course

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 };


Consumption of tokens in React components

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} />;
  }
);

Use Storybook to render React components in isolation

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: 👇️

storybook display

Enforce Accessibility rules

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