Building a design system with dark mode using React, Typescript, scss, cva and Vite - Spinner & Icon Components
Tags: react, css, typescript, vite
Published: 2023-03-21 06:04:08

### Introduction
This is part five of our series on building a complete design system from scratch. In the previous tutorial created the `Badge` component. In this short tutorial we will create 2 components Spinner and Icon which we will use in the future for the `Alert` & `Button` components. I would encourage you to play around with the deployed [storybook](http://yaldram-react-scss-chakra-ui.s3-website.ap-south-1.amazonaws.com/?path=/story/atoms-forms-button--default). All the code for this series is available on [GitHub](https://github.com/yaldram/react-scss-chakra-ui).

### Step One: Spinner Component
We want to achieve the following for the `Spinner` component - 
```tsx
<Spinner thickness="3px" speed="1s" size="sm" />
```
Under `atoms` create a new folder `feedback` and under `feedback` create a new folder called `spinner`. Under `atoms/feedback/spinner` create the `spinner.scss` file and paste the following - 
```scss
@keyframes spinner-animation {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

.spinner {
  --thickness: 2px;
  --speed: 0.4s;

  display: inline-block;
  color: currentColor;

  border-color: currentColor;
  border-style: solid;
  border-radius: 9999px;
  border-width: var(--thickness);
  border-bottom-color: transparent;
  border-left-color: transparent;

  width: 1em;
  height: 1em;

  animation: spinner-animation var(--speed) linear infinite;

  &.xs {
    width: 1.5rem;
    height: 1.5rem;
  }

  &.sm {
    width: 2rem;
    height: 2rem;
  }

  &.md {
    width: 2.5rem;
    height: 2.5rem;
  }

  &.lg {
    width: 3rem;
    height: 3rem;
  }

  &.xl {
    width: 3.5rem;
    height: 3.5rem;
  }
}
```
Notice for the thickness and speed we are using custom css properties, these will enable us to pass dynamic values at runtime. 
Now under `atoms/feedback/spinner` folder create `index.tsx` file - 
```tsx
import * as React from "react";
import { cva, VariantProps } from "class-variance-authority";

import "./spinner.scss";

const spinner = cva(["spinner"], {
  variants: {
    size: {
      xs: "xs",
      sm: "sm",
      md: "md",
      lg: "lg",
      xl: "xl",
    },
  },
});

export type SpinnerProps = VariantProps<typeof spinner> &
  React.ComponentProps<"span"> & {
    thickness?: string;
    speed?: string;
  };

export function Spinner(props: SpinnerProps) {
  const { thickness, speed, size, style, className, ...delegated } = props;

  const stylesWithVars = {
    "--thickness": thickness,
    "--speed": speed,
    ...style,
  } as React.CSSProperties;

  return (
    <span
      style={stylesWithVars}
      className={spinner({ size, className })}
      {...delegated}
    />
  );
}
```
Take a note, that we are using the `style` prop to set the `thickness and speed` custom css properties dynamically, to pass in dynamic values at runtime we have to use custom css properties. And with that our `Spinner` component is ready, you can create a story for it.

Finally, under `atoms/feedback` create a new file `index.ts` - 
```ts
export * from "./spinner";
```

### Step Two: Icon Component
Under `atoms` create a new folder `icons` under `atoms/icons` folder create five files namely `icon.scss`, `icon.tsx`, `icons.tsx`, `create-icon.tsx` and `index.ts`.

Under `atoms/icons/icon.scss` paste the following - 
```scss
.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  line-height: 1em;
  vertical-align: middle;
}
```

Under `atoms/icons/icon.tsx` paste the following - 
```tsx
import * as React from "react";
import { cva } from "class-variance-authority";

import "./icon.scss";

const fallbackIcon = {
  path: (
    <g stroke="currentColor" strokeWidth="1.5">
      <path
        strokeLinecap="round"
        fill="none"
        d="M9,9a3,3,0,1,1,4,2.829,1.5,1.5,0,0,0-1,1.415V14.25"
      />
      <path
        fill="currentColor"
        strokeLinecap="round"
        d="M12,17.25a.375.375,0,1,0,.375.375A.375.375,0,0,0,12,17.25h0"
      />
      <circle fill="none" strokeMiterlimit="10" cx="12" cy="12" r="11.25" />
    </g>
  ),
  viewBox: "0 0 24 24",
};

const icon = cva(["icon"]);

export interface IconProps extends React.ComponentProps<"svg"> {}

export const Icon = (props: IconProps) => {
  const {
    viewBox = fallbackIcon.viewBox,
    color = "currentColor",
    focusable = false,
    children,
    className,
    ...delegated
  } = props;

  const path = (children ?? fallbackIcon.path) as React.ReactNode;

  return (
    <svg
      className={icon({ className })}
      color={color}
      viewBox={viewBox}
      focusable={focusable}
      {...delegated}
    >
      {path}
    </svg>
  );
};
```

Under `atoms/icons/create-icon.tsx` paste the following - 
```tsx
import * as React from "react";

import { Icon, IconProps } from "./icon";

interface CreateIconOptions {
  viewBox?: string;
  path?: React.ReactElement | React.ReactElement[];
  d?: string;
  defaultProps?: IconProps;
}

export function createIcon(options: CreateIconOptions) {
  const {
    viewBox = "0 0 24 24",
    d: pathDefinition,
    path,
    defaultProps = {},
  } = options;

  const Component = (props: IconProps) => (
    <Icon viewBox={viewBox} {...defaultProps} {...props}>
      {path ?? <path fill="currentColor" d={pathDefinition} />}
    </Icon>
  );

  return Component;
}
```
Now we will use the `Icon` component and `createIcon` function to make some new Icons, under `atoms/feedback/icons.tsx` - 
```tsx
import * as React from "react";

import { createIcon } from "./create-icon";
import { Icon, IconProps } from "./icon";

export const CheckIcon = createIcon({
  d: "M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm6.927,8.2-6.845,9.289a1.011,1.011,0,0,1-1.43.188L5.764,13.769a1,1,0,1,1,1.25-1.562l4.076,3.261,6.227-8.451A1,1,0,1,1,18.927,8.2Z",
});

export const InfoIcon = createIcon({
  d: "M12,0A12,12,0,1,0,24,12,12.013,12.013,0,0,0,12,0Zm.25,5a1.5,1.5,0,1,1-1.5,1.5A1.5,1.5,0,0,1,12.25,5ZM14.5,18.5h-4a1,1,0,0,1,0-2h.75a.25.25,0,0,0,.25-.25v-4.5a.25.25,0,0,0-.25-.25H10.5a1,1,0,0,1,0-2h1a2,2,0,0,1,2,2v4.75a.25.25,0,0,0,.25.25h.75a1,1,0,1,1,0,2Z",
});

export const WarningIcon = createIcon({
  d: "M11.983,0a12.206,12.206,0,0,0-8.51,3.653A11.8,11.8,0,0,0,0,12.207,11.779,11.779,0,0,0,11.8,24h.214A12.111,12.111,0,0,0,24,11.791h0A11.766,11.766,0,0,0,11.983,0ZM10.5,16.542a1.476,1.476,0,0,1,1.449-1.53h.027a1.527,1.527,0,0,1,1.523,1.47,1.475,1.475,0,0,1-1.449,1.53h-.027A1.529,1.529,0,0,1,10.5,16.542ZM11,12.5v-6a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Z",
});

export const WarningTwoIcon = createIcon({
  d: "M23.119,20,13.772,2.15h0a2,2,0,0,0-3.543,0L.881,20a2,2,0,0,0,1.772,2.928H21.347A2,2,0,0,0,23.119,20ZM11,8.423a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Zm1.05,11.51h-.028a1.528,1.528,0,0,1-1.522-1.47,1.476,1.476,0,0,1,1.448-1.53h.028A1.527,1.527,0,0,1,13.5,18.4,1.475,1.475,0,0,1,12.05,19.933Z",
});

export const CloseIcon = createIcon({
  d: "M.439,21.44a1.5,1.5,0,0,0,2.122,2.121L11.823,14.3a.25.25,0,0,1,.354,0l9.262,9.263a1.5,1.5,0,1,0,2.122-2.121L14.3,12.177a.25.25,0,0,1,0-.354l9.263-9.262A1.5,1.5,0,0,0,21.439.44L12.177,9.7a.25.25,0,0,1-.354,0L2.561.44A1.5,1.5,0,0,0,.439,2.561L9.7,11.823a.25.25,0,0,1,0,.354Z",
});

export const SearchIcon = createIcon({
  d: "M23.384,21.619,16.855,15.09a9.284,9.284,0,1,0-1.768,1.768l6.529,6.529a1.266,1.266,0,0,0,1.768,0A1.251,1.251,0,0,0,23.384,21.619ZM2.75,9.5a6.75,6.75,0,1,1,6.75,6.75A6.758,6.758,0,0,1,2.75,9.5Z",
});

export const Search2Icon = createIcon({
  d: "M23.414,20.591l-4.645-4.645a10.256,10.256,0,1,0-2.828,2.829l4.645,4.644a2.025,2.025,0,0,0,2.828,0A2,2,0,0,0,23.414,20.591ZM10.25,3.005A7.25,7.25,0,1,1,3,10.255,7.258,7.258,0,0,1,10.25,3.005Z",
});

export const PhoneIcon = createIcon({
  d: "M2.20731,0.0127209 C2.1105,-0.0066419 1.99432,-0.00664663 1.91687,0.032079 C0.871279,0.438698 0.212942,1.92964 0.0580392,2.95587 C-0.426031,6.28627 2.20731,9.17133 4.62766,11.0689 C6.77694,12.7534 10.9012,15.5223 13.3409,12.8503 C13.6507,12.5211 14.0186,12.037 13.9993,11.553 C13.9412,10.7397 13.186,10.1588 12.6051,9.71349 C12.1598,9.38432 11.2304,8.47427 10.6495,8.49363 C10.1267,8.51299 9.79754,9.05515 9.46837,9.38432 L8.88748,9.96521 C8.79067,10.062 7.55145,9.24878 7.41591,9.15197 C6.91248,8.8228 6.4284,8.45491 6.00242,8.04829 C5.57644,7.64167 5.18919,7.19632 4.86002,6.73161 C4.7632,6.59607 3.96933,5.41495 4.04678,5.31813 C4.04678,5.31813 4.72448,4.58234 4.91811,4.2919 C5.32473,3.67229 5.63453,3.18822 5.16982,2.45243 C4.99556,2.18135 4.78257,1.96836 4.55021,1.73601 C4.14359,1.34875 3.73698,0.942131 3.27227,0.612963 C3.02055,0.419335 2.59457,0.0708094 2.20731,0.0127209 Z",
  viewBox: "0 0 14 14",
});

export const EmailIcon = createIcon({
  path: (
    <g fill="currentColor">
      <path d="M11.114,14.556a1.252,1.252,0,0,0,1.768,0L22.568,4.87a.5.5,0,0,0-.281-.849A1.966,1.966,0,0,0,22,4H2a1.966,1.966,0,0,0-.289.021.5.5,0,0,0-.281.849Z" />
      <path d="M23.888,5.832a.182.182,0,0,0-.2.039l-6.2,6.2a.251.251,0,0,0,0,.354l5.043,5.043a.75.75,0,1,1-1.06,1.061l-5.043-5.043a.25.25,0,0,0-.354,0l-2.129,2.129a2.75,2.75,0,0,1-3.888,0L7.926,13.488a.251.251,0,0,0-.354,0L2.529,18.531a.75.75,0,0,1-1.06-1.061l5.043-5.043a.251.251,0,0,0,0-.354l-6.2-6.2a.18.18,0,0,0-.2-.039A.182.182,0,0,0,0,6V18a2,2,0,0,0,2,2H22a2,2,0,0,0,2-2V6A.181.181,0,0,0,23.888,5.832Z" />
    </g>
  ),
});

export const ArrowForwardIcon = createIcon({
  d: "M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z",
});

export const AvatarFallback: React.FC<IconProps> = (props) => {
  return (
    <Icon
      viewBox="0 0 128 128"
      color="#fff"
      width="100%"
      height="100%"
      {...props}
    >
      <path
        fill="currentColor"
        d="M103,102.1388 C93.094,111.92 79.3504,118 64.1638,118 C48.8056,118 34.9294,111.768 25,101.7892 L25,95.2 C25,86.8096 31.981,80 40.6,80 L87.4,80 C96.019,80 103,86.8096 103,95.2 L103,102.1388 Z"
      />
      <path
        fill="currentColor"
        d="M63.9961647,24 C51.2938136,24 41,34.2938136 41,46.9961647 C41,59.7061864 51.2938136,70 63.9961647,70 C76.6985159,70 87,59.7061864 87,46.9961647 C87,34.2938136 76.6985159,24 63.9961647,24"
      />
    </Icon>
  );
};
```
Finally under `atoms/icons/index.ts` paste the following - 
```ts
export * from "./icon";
export * from "./create-icon";
export * from "./icons";
```

### Conclusion
In this tutorial we created the `Spinner` & `Icon` components, we will use these in the next tutorials. All the code for this tutorial can be found [here](https://github.com/yaldram/react-scss-chakra-ui). In the next tutorial we will create a theme able `Alert` component. Until next time PEACE.