Build Chakra UI Button component using react, typescript, styled-components and styled-system - Part 2
Tags: react, typescript
Published: 2021-09-04 08:01:19

### Introduction

Let us continue building our chakra components using `styled-components` & `styled-system`. In this tutorial we will complete cloning the Chakra UI `Button` component.

- I would like you to first check the [chakra docs] for [button](https://chakra-ui.com/docs/form/button).
- All the code for this tutorial can be found under the **atom-form-button** branch [here](https://github.com/yaldram/chakra-ui-clone).

### Prerequisite

This is Part 2 building the `Button` Component. Please check the previous post for Part One. Also please check the Chakra Button Component code [here](https://github.com/chakra-ui/chakra-ui/tree/main/packages/button/src), along with the theme / styles for the button [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/button.ts). In this we will build the actual `Button` Component, by bringing all the components we created in Part 1 together -

- Create a Button component.
- Create story for the Button component.

### Button Component

- As always let me first paste the code for you -

```tsx
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    const {
      children,
      colorScheme = "gray",
      variant = "solid",
      s = "md",
      loadingText,
      isLoading = false,
      isDisabled = false,

      spinnerPlacement = "start",
      spinner,

      rightIcon,
      leftIcon,
      iconSpacing = "0.5rem",

      ...delegated
    } = props;

    const contentProps = { rightIcon, leftIcon, iconSpacing, children };

    return (
      <BaseButton
        ref={ref}
        disabled={isDisabled || isLoading}
        colorScheme={colorScheme}
        variant={variant}
        s={s}
        {...delegated}
      >
        {isLoading && spinnerPlacement === "start" && (
          <ButtonSpinner label={loadingText} placement="start">
            {spinner}
          </ButtonSpinner>
        )}

        {isLoading ? (
          loadingText || (
            <span style={{ opacity: 0 }}>
              <ButtonContent {...contentProps} />
            </span>
          )
        ) : (
          <ButtonContent {...contentProps} />
        )}

        {isLoading && spinnerPlacement === "end" && (
          <ButtonSpinner label={loadingText} placement="end">
            {spinner}
          </ButtonSpinner>
        )}
      </BaseButton>
    );
  }
);
```

- Again as always I would like you to check Chakra's Button docs play around with all the props, you will understand the above code auto-magically.

- First and foremost we de-structure all the props, and assign default values to the props, wherever necessary.

- For `isLoading` prop we will show the `ButtonSpinner` component and place it depending on the value of the `spinnerPlacement` prop and pass the `loadingText` if we are to pass a custom loadingText.

- Look at this simple trick that chakra ui does it just sets opacity = 0 for the main `ButtonContent` when isLoading = true.

### Story

- With the above our `Button` component is completed, let us create a story.
- Under the `src/components/atoms/form/button/button.stories.tsx` file we add the below story code.
- We will create 2 stories - Playground and Default.

```tsx
import * as React from "react";

import { colorSchemeOptions } from "../../../../theme/colors";
import { Stack } from "../../layout";
import { Button, ButtonProps } from "./button";
import {
  SearchIcon,
  PhoneIcon,
  EmailIcon,
  ArrowForwardIcon,
} from "../../icons";

export default {
  title: "Atoms/Form/Button",
};

export const Playground = {
  argTypes: {
    colorScheme: {
      name: "colorScheme",
      type: { name: "string", required: false },
      defaultValue: "gray",
      description: "The Color Scheme for the button",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "gray" },
      },
      control: {
        type: "select",
        options: colorSchemeOptions,
      },
    },
    s: {
      name: "s",
      type: { name: "string", required: false },
      defaultValue: "md",
      description: "Button size height width and vertical padding",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "md" },
      },
      control: {
        type: "select",
        options: ["xs", "sm", "md", "lg"],
      },
    },
    variant: {
      name: "variant",
      type: { name: "string", required: false },
      defaultValue: "solid",
      description: "Button variants",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "solid" },
      },
      control: {
        type: "select",
        options: ["link", "outline", "solid", "ghost", "unstyled"],
      },
    },
    isLoading: {
      name: "isLoading",
      type: { name: "boolean", required: false },
      defaultValue: false,
      description: "Pass the isLoading prop to show loading state.",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "false" },
      },
    },
    loadingText: {
      name: "loadingText",
      type: { name: "string", required: false },
      defaultValue: "",
      description: "Prop to show a spinner and the loading text.",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "-" },
      },
    },
    spinnerPlacement: {
      name: "spinnerPlacement",
      type: { name: "string", required: false },
      defaultValue: "start",
      description: `When a loadingText is present, you can change the
      placement of the spinner element to either start or end.
      It is start by default.`,
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "start" },
      },
      control: {
        type: "select",
        options: ["start", "end"],
      },
    },
    isDisabled: {
      name: "isDisabled",
      type: { name: "boolean", required: false },
      defaultValue: false,
      description: "Pass the isDisable prop to show disabled state.",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "false" },
      },
    },
    isFullWidth: {
      name: "isFullWidth",
      type: { name: "boolean", required: false },
      defaultValue: false,
      description: "If true will expand full width.",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "false" },
      },
    },
  },
  render: (args: ButtonProps) => <Button {...args}>Button</Button>,
};

export const Default = {
  render: () => (
    <Stack direction="column" spacing="xl">
      <Stack spacing="lg" align="center">
        <Button colorScheme="teal" s="xs">
          Button
        </Button>
        <Button colorScheme="teal" s="sm">
          Button
        </Button>
        <Button colorScheme="teal" s="md">
          Button
        </Button>
        <Button colorScheme="teal" s="lg">
          Button
        </Button>
      </Stack>
      <Stack spacing="lg" align="center">
        <Button colorScheme="teal" variant="solid">
          Button
        </Button>
        <Button colorScheme="teal" variant="outline">
          Button
        </Button>
        <Button colorScheme="teal" variant="ghost">
          Button
        </Button>
        <Button colorScheme="teal" variant="link">
          Button
        </Button>
      </Stack>
      <Stack spacing="lg">
        <Button isLoading colorScheme="teal" variant="solid">
          Email
        </Button>
        <Button
          isLoading
          loadingText="Loading"
          colorScheme="teal"
          variant="outline"
          spinnerPlacement="start"
        >
          Submit
        </Button>
        <Button
          isLoading
          loadingText="Loading"
          colorScheme="teal"
          variant="outline"
          spinnerPlacement="end"
        >
          Continue
        </Button>
      </Stack>
      <Stack spacing="lg">
        <Button leftIcon={<EmailIcon />} colorScheme="teal" variant="solid">
          Email
        </Button>
        <Button
          rightIcon={<ArrowForwardIcon />}
          colorScheme="teal"
          variant="outline"
        >
          Call us
        </Button>
      </Stack>
    </Stack>
  ),
};
```

- Now run `npm run storybook` check the stories. Under the Playground stories check the controls section play with the props, add more controls if you like.

### Build the Library

- Under the `/button/index.ts` file paste the following -

```tsx
export * from "./button";
```

- Under the `/form/index.ts` file paste the following -

```tsx
export * from "./button";
```

- Under the `/atom/index.ts` file paste the following -

```tsx
export * from "./layout";
export * from "./typography";
export * from "./feedback";
export * from "./icon";
export * from "./icons";
export * from "./form";
```

- Now `npm run build`.

- Under the folder `example/src/App.tsx` we can test our `Button` component. Copy paste the default story code and run `npm run start` from the `example` directory.

### Summary

There you go guys our Button component just is completed. You can find the code for this tutorial under the **atom-form-button** branch [here](https://github.com/yaldram/chakra-ui-clone). In the next tutorial we will create `IconButton` component. Until next time PEACE.