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/atoms
folder create a new folder calledform
.Under form folder create another folder called
button
. Under it create 4 filesbutton.tsx
,button-icon.tsx
,button-spinner.tsx
andindex.ts
.Also under
components/atom/form
create a new fileindex.ts
.So our folder structure stands like - src/components/atoms/form/button.
ButtonIcon Component
- Under
components/atom/form/button-icon.tsx
paste 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
BaseSpan
styled component and passed inspace
utility fromstyled-system
. This will allow us to pass marginProps like ml and mr to our component.
Button Spinner
- Under
components/atom/form/button-spinner.tsx
paste 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
ButtonSpinner
component is used when we pass theisLoading
prop to theButton
component. It also handles the case in which we pass a custom Spinner using thespinner
prop, 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
, namelyvariant
for off-course the variant - "solid, outline" ands
for the size of the button, I choses
so that it won't conflict with the size prop that comes with styled-systemlayout
utility function.We will start by first creating our variant types -
ButtonSizes
andButtonVariants
.Then we will create our
ButtonOptions
&ButtonProps
.Under
components/atom/form/button.tsx
paste 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
ButtonOptions
we have covered all the props that chakra's originalButton
takes 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
&colorScheme
props you will get to know what we did above.For the variant prop we are depending on the
colorScheme
passed, picking a shade from the theme. Notice I usedbg
instead ofbackground
which means these styled-system shorthands also work in the variant function.For the solid variant notice the
yellow
andcyan
variants 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.tsx
paste 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)