DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI Heading component using react, typescript, styled-components and styled-system

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.
  • 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 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. And the associated chakra theme / styles setup for Heading Component here.

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 -
git checkout -b atom-typography-heading
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • 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. 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.
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>,
};
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 /typography/index.ts file and paste the following -
export * from "./text";
export * from "./heading";
Enter fullscreen mode Exit fullscreen mode
  • 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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 -
<Heading variant="lg" fontSize={["50px", "50px"]}>
  I'm overriding this heading
</Heading>
Enter fullscreen mode Exit fullscreen mode
  • 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 -

<Heading variant="lg" fontSize={["35px", null, "70px"]}>
  I'm overriding this heading
</Heading>
Enter fullscreen mode Exit fullscreen mode
  • 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.

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. In the next tutorial we will create a Spinner component. Until next time PEACE.

Discussion (0)