Introduction
Let us continue building our chakra components using styled-components & styled-system. In this tutorial we will be 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
Please check the previous post where we have completed the Icon Components. Also please check the Chakra Button Component code here, along with the theme / styles for the button here
After checking the docs for the Button component, you know that it takes in quite a few props like isLoading, rightIcon, leftIcon, spinner, etc. Internally we have multiple components that handle these scenarios. So in this tutorial we will -
- Create a ButtonIcon component.
 - Create a ButtonSpinner component.
 - Create a BaseButton styled component.
 - Create a ButtonContent component.
 
And in the next tutorial we will build the actual Button Component, by bringing all these together. So in this tutorial if you don't get the actual component logic no problem, everything will be clear in the next tutorial.
Setup
- First let us create a branch, from the main branch run -
 
git checkout -b atom-form-button
Under the
components/atomsfolder create a new folder calledform.Under form folder create another folder called
button. Under it create 4 filesbutton.tsx,button-icon.tsx,button-spinner.tsxandindex.ts.Also under
components/atom/formcreate a new fileindex.ts.So our folder structure stands like - src/components/atoms/form/button.
ButtonIcon Component
- Under 
components/atom/form/button-icon.tsxpaste the following code - 
import * as React from "react";
import styled from "styled-components";
import { space, SpaceProps } from "styled-system";
export interface ButtonIconProps extends SpaceProps {
  children?: React.ReactNode;
}
const BaseSpan = styled.span<ButtonIconProps>`
  ${space}
`;
export const ButtonIcon: React.FC<ButtonIconProps> = (props) => {
  const { children, ...delegated } = props;
  const componentChildren = React.isValidElement(children)
    ? React.cloneElement(children, {
        "aria-hidden": true,
        focusable: false,
      })
    : children;
  return <BaseSpan {...delegated}>{componentChildren}</BaseSpan>;
};
- We created a 
BaseSpanstyled component and passed inspaceutility fromstyled-system. This will allow us to pass marginProps like ml and mr to our component. 
Button Spinner
- Under 
components/atom/form/button-spinner.tsxpaste the following code - 
import * as React from "react";
import { Box, BoxProps } from "../../layout";
import { Spinner } from "../../feedback";
interface ButtonSpinnerProps extends BoxProps {
  label?: string;
  placement?: "start" | "end";
}
export const ButtonSpinner: React.FC<ButtonSpinnerProps> = (props) => {
  const {
    label,
    placement,
    children = <Spinner color="currentColor" />,
    ...delegated
  } = props;
  const marginProp = placement === "start" ? "marginRight" : "marginLeft";
  const spinnerStyles = {
    display: "flex",
    fontSize: "1em",
    lineHeight: "normal",
    alignItems: "center",
    position: label ? "relative" : "absolute",
    [marginProp]: label ? "0.5rem" : 0,
  };
  return (
    <Box {...spinnerStyles} {...delegated}>
      {children}
    </Box>
  );
};
- The above 
ButtonSpinnercomponent is used when we pass theisLoadingprop to theButtoncomponent. It also handles the case in which we pass a custom Spinner using thespinnerprop, if we don't pass a custom Spinner it will use the default one we created before. 
Styled Button Component
We will be creating 2 variant props for our
Button, namelyvariantfor off-course the variant - "solid, outline" andsfor the size of the button, I chosesso that it won't conflict with the size prop that comes with styled-systemlayoututility function.We will start by first creating our variant types -
ButtonSizesandButtonVariants.Then we will create our
ButtonOptions&ButtonProps.Under
components/atom/form/button.tsxpaste the following code -
import * as React from "react";
import styled from "styled-components";
import {
  compose,
  variant as variantFun,
  color,
  border,
  layout,
  space,
  fontSize,
  ResponsiveValue,
  SpaceProps,
  ColorProps,
  BorderProps,
  FontSizeProps,
  LayoutProps,
} from "styled-system";
import { ColorScheme as ButtonColorScheme } from "../../../../theme/colors";
import { ButtonSpinner } from "./button-spinner";
import { ButtonIcon } from "./button-icon";
type ButtonSizes = "xs" | "sm" | "md" | "lg";
type ButtonVariants = "link" | "outline" | "solid" | "ghost" | "unstyled";
interface ButtonOptions {
  colorScheme?: ButtonColorScheme;
  s?: ResponsiveValue<ButtonSizes>;
  variant?: ResponsiveValue<ButtonVariants>;
  isLoading?: boolean;
  isActive?: boolean;
  isDisabled?: boolean;
  isFullWidth?: boolean;
  loadingText?: string;
  leftIcon?: React.ReactElement;
  rightIcon?: React.ReactElement;
  iconSpacing?: SpaceProps["marginRight"];
  spinner?: React.ReactElement;
  spinnerPlacement?: "start" | "end";
}
export type ButtonProps = ColorProps &
  BorderProps &
  FontSizeProps &
  LayoutProps &
  ButtonOptions &
  SpaceProps &
  React.ComponentPropsWithoutRef<"button"> & { children?: React.ReactNode };
Under the
ButtonOptionswe have covered all the props that chakra's originalButtontakes in.For the StyledButton let me first paste the code and we will go over it one by one -
function variantGhost(colorScheme: ButtonColorScheme) {
  if (colorScheme === "gray") {
    return {
      color: "inherit",
      "&:hover": {
        bg: "gray100",
      },
      "&:active": {
        bg: "gray200",
      },
    };
  }
  return {
    color: `${colorScheme}600`,
    bg: "transparent",
    "&:hover": {
      bg: `${colorScheme}50`,
    },
    "&:active": {
      bg: `${colorScheme}100`,
    },
  };
}
function variantOutline(colorScheme: ButtonColorScheme) {
  return {
    border: "1px solid",
    borderColor: colorScheme === "gray" ? "gray200" : "currentColor",
    ...variantGhost(colorScheme),
  };
}
function variantSolid(colorScheme: ButtonColorScheme) {
  const accessibleColorMap = {
    yellow: {
      background: "yellow400",
      componentColor: "black",
      hoverBg: "yellow500",
      activeBg: "yellow600",
    },
    cyan: {
      background: "cyan400",
      componentColor: "black",
      hoverBg: "cyan500",
      activeBg: "cyan600",
    },
  };
  if (colorScheme === "gray") {
    return {
      bg: "gray100",
      "&:hover": {
        bg: "gray200",
        "&:disabled": { bg: "gray100" },
      },
      "&:active": { bg: "gray300" },
    };
  }
  const {
    background = `${colorScheme}500`,
    componentColor = "white",
    hoverBg = `${colorScheme}600`,
    activeBg = `${colorScheme}700`,
  } = accessibleColorMap[colorScheme] || {};
  return {
    bg: background,
    color: componentColor,
    "&:hover": {
      bg: hoverBg,
      "&:disabled": { bg: background },
    },
    "&:active": { bg: activeBg },
  };
}
function variantLink(colorScheme: ButtonColorScheme) {
  return {
    padding: 0,
    background: "none",
    height: "auto",
    lineHeight: "normal",
    verticalAlign: "baseline",
    color: `${colorScheme}500`,
    "&:hover": {
      textDecoration: "underline",
      "&:disabled": {
        textDecoration: "none",
      },
    },
    "&:active": {
      color: `${colorScheme}700`,
    },
  };
}
function variantUnStyled() {
  return {
    background: "none",
    color: "inherit",
    display: "inline",
    lineHeight: "inherit",
    margin: 0,
    p: 0,
  };
}
function variantSizes() {
  return {
    lg: {
      height: "3rem",
      minWidth: "3rem",
      fontSize: "lg",
      paddingLeft: "lg",
      paddingRight: "lg",
    },
    md: {
      height: "2.5rem",
      minWidth: "2.5rem",
      fontSize: "md",
      paddingLeft: "md",
      paddingRight: "md",
    },
    sm: {
      height: "2rem",
      minWidth: "2rem",
      fontSize: "sm",
      paddingLeft: "sm",
      paddingRight: "sm",
    },
    xs: {
      height: "1.5rem",
      minWidth: "1.5rem",
      fontSize: "xs",
      paddingLeft: "xs",
      paddingRight: "xs",
    },
  };
}
const BaseButton = styled.button<ButtonProps>`
  border: none;
  outline: none;
  font-family: inherit;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.25em 0.75em;
  font-weight: 500;
  text-align: center;
  line-height: 1.1;
  transition: 220ms all ease-in-out;
  border-radius: 0.375rem;
  width: ${({ isFullWidth }) => (isFullWidth ? "100%" : "auto")};
  &:hover {
    &:disabled {
      background: initial;
    }
  }
  &:focus {
    box-shadow: outline;
  }
  &:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    box-shadow: none;
  }
  ${({ colorScheme = "gray" }) =>
    variantFun({
      prop: "variant",
      variants: {
        link: variantLink(colorScheme),
        outline: variantOutline(colorScheme),
        solid: variantSolid(colorScheme),
        ghost: variantGhost(colorScheme),
        unstyled: variantUnStyled(),
      },
    })}
  ${variantFun({
    prop: "s",
    variants: variantSizes(),
  })}
  ${compose(color, border, layout, space, fontSize)}
`;
Now I can write a lot explaining the above, but I would suggest one thing read the chakra docs play around with the
variant&colorSchemeprops you will get to know what we did above.For the variant prop we are depending on the
colorSchemepassed, picking a shade from the theme. Notice I usedbginstead ofbackgroundwhich means these styled-system shorthands also work in the variant function.For the solid variant notice the
yellowandcyanvariants have a black color and light background we handled that case using a simple objectaccessibleColorMap.Also notice that I called
compose()after thevariant()function calls, this is done so that I can overwrite the styles. Say we have a button which variant = solid, colorScheme = 'orange' and s = 'md' for some reason I want the orange to be more dark say orang900 while keeping the other values same I can simply overwrite my variant bg color like below - why because specificity matters.
<Button colorScheme="orange" variant="solid" s="md" bg="orange900">
  Button
</Button>
- For the base button styles check this awesome post here, highly recommended.
 
ButtonContent Component
Last component for this tutorial I promise.
Under
components/atom/form/button.tsxpaste the following code -
type ButtonContentProps = Pick<
  ButtonProps,
  "leftIcon" | "rightIcon" | "children" | "iconSpacing"
>;
function ButtonContent(props: ButtonContentProps) {
  const { leftIcon, rightIcon, children, iconSpacing } = props;
  return (
    <React.Fragment>
      {leftIcon && <ButtonIcon mr={iconSpacing}>{leftIcon}</ButtonIcon>}
      {children}
      {rightIcon && <ButtonIcon ml={iconSpacing}>{rightIcon}</ButtonIcon>}
    </React.Fragment>
  );
}
Summary
This was quite long I guess, believe me guys you will understand everything in the next tutorial where we will bring all these components together. You can find the code for this tutorial under the atom-form-button branch here. In the next tutorial we will create Button component. Until next time PEACE.
    
Top comments (0)