DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI Button component using react, typescript, styled-components and styled-system - Part 2

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.
  • All the code for this tutorial can be found under the atom-form-button branch here.

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, along with the theme / styles for the button here. 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 -
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>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • 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.
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • 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 -
export * from "./button";
Enter fullscreen mode Exit fullscreen mode
  • Under the /form/index.ts file paste the following -
export * from "./button";
Enter fullscreen mode Exit fullscreen mode
  • Under the /atom/index.ts file paste the following -
export * from "./layout";
export * from "./typography";
export * from "./feedback";
export * from "./icon";
export * from "./icons";
export * from "./form";
Enter fullscreen mode Exit fullscreen mode
  • 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. In the next tutorial we will create IconButton component. Until next time PEACE.

Discussion (0)