Build Chakra UI Heading component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-03 11:57:55

### Introduction

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

- I would like you to first check the chakra docs for [heading](https://chakra-ui.com/docs/typography/heading).
- We will create the `Heading` component from scratch, just like the `Text` component. We could have extended the Text component itself but unfortunately, `isTruncated` and `noOfLines` props did not work as expected.
- All the code for this tutorial can be found [here](https://github.com/yaldram/chakra-ui-clone) under the **atom-typography-heading** branch.

### Prerequisite

Please check the previous post where we have completed the Heading Component. Also please check the Chakra Heading Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/layout/src/heading.tsx). And the associated chakra theme / styles setup for Heading Component [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/heading.ts).

In this tutorial we will -

- Create a Heading component.
- Create stories for the Heading component.

### Setup

- First let us create a branch, from the main branch run -

```bash
git checkout -b atom-typography-heading
```

- Create a new folder under the `typography` folder called `heading`.

- Under the `atoms/typography/heading` folder we will create 2 files `index.tsx` & `heading.stories.tsx`.

- So our folder structure stands like - **src/components/atoms/typography/heading**.

### Heading Component

- It is similar to the `Text` component, the only addition is heading variants.

- Paste the following code -

```tsx
import * as React from "react";
import styled, { CSSProperties } from "styled-components";
import shouldForwardProp from "@styled-system/should-forward-prop";
import {
  compose,
  display,
  space,
  typography,
  color,
  colorStyle,
  borderRadius,
  layout,
  system,
  variant as variantFun,
  DisplayProps,
  SpaceProps,
  TypographyProps,
  ColorProps,
  ColorStyleProps,
  BorderRadiusProps,
  LayoutProps,
  ResponsiveValue,
} from "styled-system";

type VariantSize = "sm" | "md" | "lg" | "xl" | "2xl" | "xs" | "3xl" | "4xl";

interface TextOptions {
  isTruncated?: boolean;
  noOfLines?: number;
  variant?: ResponsiveValue<VariantSize>;
  whiteSpace?: ResponsiveValue<CSSProperties["whiteSpace"]>;
  textOverflow?: ResponsiveValue<CSSProperties["textOverflow"]>;
  decoration?: ResponsiveValue<CSSProperties["textDecoration"]>;
  transform?: ResponsiveValue<CSSProperties["textTransform"]>;
}

export type HeadingProps = DisplayProps &
  SpaceProps &
  TypographyProps &
  ColorProps &
  ColorStyleProps &
  BorderRadiusProps &
  LayoutProps &
  React.ComponentPropsWithoutRef<"h2"> &
  TextOptions & {
    as?: React.ElementType;
    children?: React.ReactNode;
  };

const BaseHeading = styled.h2.withConfig({
  shouldForwardProp,
})<HeadingProps>`
  ${variantFun({
    prop: "variant",
    variants: {
      "4xl": {
        fontSize: ["6xl", null, "7xl"],
        lineHeight: "none",
      },
      "3xl": {
        fontSize: ["5xl", null, "6xl"],
        lineHeight: "none",
      },
      "2xl": {
        fontSize: ["4xl", null, "5xl"],
        lineHeight: ["shorter", null, "none"],
      },
      xl: {
        fontSize: ["3xl", null, "4xl"],
        lineHeight: ["short", null, "shorter"],
      },
      lg: {
        fontSize: ["2xl", null, "3xl"],
        lineHeight: ["short", null, "shorter"],
      },
      md: { fontSize: "xl", lineHeight: "shorter" },
      sm: { fontSize: "md", lineHeight: "shorter" },
      xs: { fontSize: "sm", lineHeight: "shorter" },
    },
  })}
  ${compose(
    space,
    display,
    typography,
    color,
    colorStyle,
    borderRadius,
    layout,
    system({
      whiteSpace: true,
      textOverflow: true,
      decoration: {
        property: "textDecoration",
      },
      transform: {
        property: "textTransform",
      },
    })
  )}
  ${({ noOfLines }) =>
    noOfLines &&
    `
    display: -webkit-box;
    line-clamp: ${noOfLines};
    overflow: hidden;
    -webkit-line-clamp: ${noOfLines};
    -webkit-box-orient: vertical;
  `};
`;

export const Heading = React.forwardRef<HTMLParagraphElement, HeadingProps>(
  (props, ref) => {
    const { children, variant = "md", isTruncated, ...delegated } = props;

    const truncatedProps = isTruncated
      ? {
          textOverflow: "ellipsis",
          overflow: "hidden",
          whiteSpace: "nowrap",
        }
      : {};

    return (
      <BaseHeading
        ref={ref}
        variant={variant}
        {...truncatedProps}
        {...delegated}
      >
        {children}
      </BaseHeading>
    );
  }
);
```

- I would like to draw your attention towards the variant function. Check for fontSizes and lineHeights we are passing arrays, these are responsive styles [docs](https://styled-system.com/responsive-styles/). We can use responsive styles in our variants too.

- Also check we are passing the theme tokens (like "2xl", "shorter") and not raw values (like "2rem", "1").

### Story

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

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

import { Heading, HeadingProps } from ".";

export default {
  title: "Atoms/Typography/Heading",
};

export const Playground = {
  argTypes: {
    variant: {
      name: "variant",
      type: { name: "string", required: false },
      defaultValue: "md",
      description: `Responsive Values.
      The font size of the heading will
      automatically decrease in size for smaller screens`,
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "md" },
      },
      control: {
        type: "select",
        options: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"],
      },
    },
    isTruncated: {
      name: "isTruncated",
      type: { name: "boolean", required: false },
      defaultValue: false,
      description: "Truncate Text.",
      table: {
        type: { summary: "boolean" },
        defaultValue: { summary: "false" },
      },
    },
    noOfLines: {
      name: "noOfLines",
      type: { name: "number", required: false },
      defaultValue: "0",
      description: "Number of Lines to show",
      table: {
        type: { summary: "number" },
        defaultValue: { summary: "-" },
      },
    },
    as: {
      name: "as",
      type: { name: "string", required: false },
      defaultValue: "h2",
      description: "Element type to render.",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "h2" },
      },
      control: {
        type: "select",
        options: ["h1", "h2", "h3", "h4", "h5", "h6"],
      },
    },
  },
  render: (args: HeadingProps) => {
    return <Heading {...args}>In love with React & Next and Gatsby.</Heading>;
  },
};

export const Default = {
  render: () => <Heading>I am a Heading</Heading>,
};
```

- 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 `/typography/index.ts` file and paste the following -

```tsx
export * from "./text";
export * from "./heading";
```

- Now `npm run build`.

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

```tsx
import * as React from "react";
import { Stack, Heading } from "chakra-ui-clone";

export function App() {
  return (
    <Stack direction="column" spacing="lg">
      <Heading as="h1" variant="4xl" isTruncated>
        (4xl) In love with React & Next
      </Heading>
      <Heading as="h2" variant="3xl" isTruncated>
        (3xl) In love with React & Next
      </Heading>
      <Heading as="h2" variant="2xl">
        (2xl) In love with React & Next
      </Heading>
      <Heading as="h2" variant="xl">
        (xl) In love with React & Next
      </Heading>
      <Heading as="h3" variant="lg">
        (lg) In love with React & Next
      </Heading>
      <Heading as="h4" variant="md">
        (md) In love with React & Next
      </Heading>
      <Heading as="h5" variant="sm">
        (sm) In love with React & Next
      </Heading>
      <Heading as="h6" variant="xs">
        (xs) In love with React & Next
      </Heading>
      <Heading variant="lg" fontSize={["50px", "50px"]}>
        I'm overriding this heading
      </Heading>
    </Stack>
  );
}
```

### Caveats

- If you are to over-ride say the "fontSize" of the Heading of variant say "lg", now given the fact that our variant is written in a responsive setting i.e. using an array to override it we need to use an array like so -

```tsx
<Heading variant="lg" fontSize={["50px", "50px"]}>
  I'm overriding this heading
</Heading>
```

- This can be powerful and limiting at the same time.

- Limiting in the sense say I want to apply the styles of variant lg but want a fontSize of 50px I have to pass an array instead of a normal string fontSize="50px" which will only apply to a certain width just for the first breakpoint mentioned under `theme.breakpoints`.

- Powerful, because we can override the responsive styles, lets say we want to override the fontSize for the second breakpoint we do -

```tsx
<Heading variant="lg" fontSize={["35px", null, "70px"]}>
  I'm overriding this heading
</Heading>
```

- Keep in mind the first element of the array is the default size, that will apply to all breakpoints for which we don't provide a value or pass a null. More on that [here](https://styled-system.com/responsive-styles/).

### Summary

There you go guys in this tutorial we created `Heading` component just like chakra ui and stories for them. You can find the code for this tutorial under the **atom-typography-heading** branch [here](https://github.com/yaldram/chakra-ui-clone). In the next tutorial we will create a Spinner component. Until next time PEACE.