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

### Introduction

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

- I would like you to first check the chakra docs for [text](https://chakra-ui.com/docs/typography/text).
- We will create the `Text` component from scratch.
- All the code for this tutorial can be found [here](https://github.com/yaldram/chakra-ui-clone) under the **atom-typography-text** branch.

### Prerequisite

Please check the previous post where we have completed the Grid Components. Also please check the Chakra Text Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/layout/src/text.tsx).

In this tutorial we will -

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

### Setup

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

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

- Create a new folder under the `atoms` folder called `typography`. Under the `typography` folder create a new folder called `text`.

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

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

### Text Component

- For our Text component we have 2 additional props, `noOfLines` and `isTruncated`. Given noOfLines = 2, it will truncate the text and add an ellipses (...) on the second line. Similarly if we are to pass isTruncated it will limit the text only to one line and add an ellipses (...).

- Let me 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,
  borderRadius,
  layout,
  system,
  DisplayProps,
  SpaceProps,
  TypographyProps,
  ColorProps,
  BorderRadiusProps,
  LayoutProps,
  ResponsiveValue,
} from "styled-system";

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

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

const BaseText = styled.p.withConfig({
  shouldForwardProp,
})<TextProps>`
  ${compose(space, display, typography, color, 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 Text = React.forwardRef<HTMLParagraphElement, TextProps>(
  (props, ref) => {
    const { children, isTruncated, ...delegated } = props;

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

    return (
      <BaseText ref={ref} {...truncatedProps} {...delegated}>
        {children}
      </BaseText>
    );
  }
);

export const VisuallyHiddenText = styled.span`
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
`;
```

- Notice we have added / extended our system with the following props, whiteSpace, textOverflow, decoration  (text-decoration), transform (text-transform).

- Also we have regular style utility functions from styled-system like space, display, typography, layout, etc.

- Also check that we have intercepted the isTruncated props and we conditionally add necessary props.

- We also have an additional `VisuallyHiddenText` which we will use for accessibility purposes, example say we have a checkbox with a label and we want the screen reader to read some different text.

- Under `src/theme/typography.ts` paste the following -

```ts
export function fontSizesOptions() {
  const options = Object.keys(typography.fontSizes);
  const labels = Object.entries(typography.fontSizes).reduce(
    (acc, [key, value]) => {
      acc[key] = `${key} (${value})`;
      return acc;
    },
    {}
  );

  return { options, labels };
}
```

### Story

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

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

import { fontSizesOptions } from "../../../../theme/typography";
import { Text, TextProps } from ".";

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

export const Playground = {
  argTypes: {
    fontSize: {
      name: "fontSize",
      type: { name: "string", required: false },
      defaultValue: "md",
      description: "Font Size from theme.fontSizes",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "-" },
      },
      control: {
        type: "select",
        ...fontSizesOptions(),
      },
    },
    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: "-" },
      },
    },
  },
  render: (args: TextProps) => (
    <Text {...args}>
      Lorem ipsum is placeholder text commonly used in the graphic, print, and
      publishing industries for previewing layouts and visual mockups.
    </Text>
  ),
};

export const Default = {
  render: () => (
    <Text>
      Text component is the used to render text and paragraphs within an
      interface. It renders a p tag by default.
    </Text>
  ),
};

export const TextTypes = {
  argTypes: {
    as: {
      name: "as",
      type: { name: "string", required: false },
      defaultValue: "p",
      description: "Element type to render.",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "p" },
      },
      control: {
        type: "select",
        options: [
          "i",
          "u",
          "abbr",
          "cite",
          "del",
          "em",
          "ins",
          "kbd",
          "mark",
          "s",
          "samp",
          "sub",
          "sup",
        ],
      },
    },
  },
  render: (args: TextProps) => <Text {...args}>Sample Text</Text>,
};
```

- 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.

- Also check the TextTypes story we are using the styled-component polymorphic `as prop`.

### Build the Library

- Under `atom/typography` folder create an `index.ts` file.

- Under the `/typography/index.ts` file and paste the following -

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

- Under the `atom/index.ts` file paste the following -

```tsx
export * from "./layout";
export * from "./typography";
```

- 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 { Text } from "chakra-ui-clone";

export function App() {
  return (
    <>
      <Text fontSize="50px" color="tomato">
        I'm using a custom font-size value for this text
      </Text>
      <Text as="i">Italic</Text>
      <br />
      <Text as="u">Underline</Text>
      <br />
      <Text as="s">Strike Through</Text>
    </>
  );
}
```

### Summary

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