Build Chakra UI Avatar component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-05 16:32:06

### Introduction

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

- I would like you to first check the chakra docs for [avatar](https://chakra-ui.com/docs/media-and-icons/avatar).
- All the code for this tutorial can be found under the **atom-avatar** branch [here](https://github.com/yaldram/chakra-ui-clone).

### Prerequisite

Please check the previous post where we have completed the `Image` component and `useImage` hook. Also please check the Chakra Avatar Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/avatar/src/avatar.tsx). The theme / styles are [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/avatar.ts) In this tutorial we will -

- Create a Avatar component.
- Create story for the Avatar component.

### Setup

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

```bash
git checkout -b atom-avatar
```

- Under the `components/atoms` folder create a new folder called `avatar`. Under `avatar` folder create 3 files `avatar.tsx`, `avatar.stories.tsx` and `index.ts`.

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

### AvatarBadge Component

- First we will need to install the following package -

```bash
npm install tinycolor2
npm install --save-dev @types/tinycolor2
```

- Under the `utils` folder create a new file `theme.ts` and paste the following code -

```ts
import Color from "tinycolor2";

export function randomColor() {
  return Color.random().toHexString();
}

export function isDark(colorHex: string) {
  return Color(colorHex).isDark();
}

export function transparentize(color: string, opacity: number) {
  return Color(color).setAlpha(opacity).toRgbString();
}
```

- If you played with the chakra Avatar component on the docs site. You might have noticed that there are 3 components, we need to create `Avatar`, `AvatarBadge` and `AvatarGroup`. We will create `AvatarGroup` in the next tutorial. Internally we have another component called `AvatarName` & `AvatarImage`.

- For the `AvatarBadge` paste the following -

```tsx
export interface AvatarBadgeProps extends BoxProps {}

const BaseAvatarBadge = styled(Box)<AvatarBadgeProps>`
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  bottom: 0;
  transform: translate(55%, 35%);
  border-radius: 9999px;
`;

export const AvatarBadge = React.forwardRef<HTMLDivElement, AvatarBadgeProps>(
  (props, ref) => {
    const {
      borderWidth = "0.2em",
      borderStyle = "solid",
      borderColor = "white",
      ...delegated
    } = props;

    return (
      <BaseAvatarBadge
        ref={ref}
        borderWidth={borderWidth}
        borderStyle={borderStyle}
        borderColor={borderColor}
        {...delegated}
      />
    );
  }
);
```

- We are extending `Box` with some default styles and passing some styles as props with default values.

### AvatarName Component

- Paste the following code -

```tsx
function initials(name: string) {
  const [firstName, lastName] = name.split(" ");
  return firstName && lastName
    ? `${firstName.charAt(0)}${lastName.charAt(0)}`
    : firstName.charAt(0);
}

interface AvatarNameProps
  extends BoxProps,
    Pick<AvatarOptions, "name" | "getInitials"> {}

const AvatarName: React.FC<AvatarNameProps> = (props) => {
  const { name, getInitials, ...delegated } = props;

  return <Box {...delegated}>{name ? getInitials?.(name) : null}</Box>;
};
```

### AvatarOptions Type

- Paste the following code before the `AvatarBadge` component -

```ts
interface AvatarOptions {
  name?: string;
  showBorder?: boolean;
  src?: string;
  srcSet?: string;
  loading?: "eager" | "lazy";
  onError?: () => void;
  icon?: React.ReactElement;
  getInitials?: (name: string) => string;
}
```

- Inline with the chakra Avatar component we have the above Props that we will pass to Avatar : -

  - name - The name of the person in the avatar. If `src` has loaded, the name will be used as the `alt` attribute of the `img`. If `src` is not loaded, the name will be used to create the initials.
  - showBorder - If `true`, the `Avatar` will show a border around it. Best for a group of avatars.
  - src - The image url of the `Avatar`.
  - srcSet - List of sources to use for different screen resolutions.
  - loading - Defines loading strategy for image either "loading" | "eager".
  - onError() - Function called when image failed to load.
  - icon - The default avatar used as fallback when `name`, and `src` is not specified.
  - getInitials(name: string) - Function to get the initials to display

- Did you find some image related props similar to the `Image` component / `useImage` hook params we completed last tutorial, we will be using the `useImage` hook for `AvatarImage`.

### AvatarImage Component

- Paste the following code below `AvatarName` component -

```tsx
export type AvatarSizes =
  | "2xs"
  | "xs"
  | "sm"
  | "md"
  | "lg"
  | "xl"
  | "2xl"
  | "full";

export interface AvatarProps
  extends Omit<BoxProps, "onError" | "size">,
    AvatarOptions {
  iconLabel?: string;
  s?: ResponsiveValue<AvatarSizes>;
}

interface AvatarImageProps
  extends ImageProps,
    Pick<AvatarProps, "getInitials" | "borderRadius" | "icon" | "name"> {
  iconLabel?: string;
}

const AvatarImage: React.FC<AvatarImageProps> = (props) => {
  const {
    src,
    onError,
    getInitials,
    name,
    borderRadius,
    loading,
    iconLabel,
    icon = <AvatarFallback />,
  } = props;

  const status = useImage({ src, onError });

  const hasLoaded = status === "loaded";

  const showFallback = !src || !hasLoaded;

  if (showFallback) {
    return name ? (
      <AvatarName getInitials={getInitials} name={name} />
    ) : (
      React.cloneElement(icon, {
        role: "img",
        "aria-label": iconLabel,
      })
    );
  }

  return (
    <Image
      src={src}
      alt={name}
      loading={loading}
      boxSize="100%"
      fit="cover"
      borderRadius={borderRadius ?? "2px"}
    />
  );
};
```

- We will apply the fallback Icon (showFallback) under 2 conditions -

  - If `src` was passed and the image has not loaded or failed to load.
  - If `src` wasn't passed.

- If `src` was passed and the image has loaded, we'll return the <Image> Component.

### Avatar Component

- Paste the Following code below the `AvatarImage` component -

```tsx
const BaseAvatar = styled(Box)<AvatarProps>`
  ${({ name, bg, backgroundColor, theme: { colors } }) => {
    const avatarBg = name ? randomColor() : colors.gray400;
    const color = isDark(avatarBg) ? "white" : colors.gray800;

    return {
      backgroundColor: backgroundColor ?? bg ?? avatarBg,
      color,
      borderColor: "white",
      verticalAlign: "top",
      display: "inline-flex",
      alignItems: "center",
      justifyContent: "center",
      textAlign: "center",
      textTransform: "uppercase",
      fontWeight: 500,
      position: "relative",
      flexShrink: 0,
    };
  }}

  ${variant({
    prop: "s",
    variants: {
      "2xs": {
        size: "1rem",
        fontSize: "calc(1rem / 2.5)",
      },
      xs: {
        size: "1.5rem",
        fontSize: "calc(1.5rem / 2.5)",
      },
      sm: {
        size: "2rem",
        fontSize: "calc(2rem / 2.5)",
      },
      md: {
        size: "3rem",
        fontSize: "calc(3rem / 2.5)",
      },
      lg: {
        size: "4rem",
        fontSize: "calc(4rem / 2.5)",
      },
      xl: {
        size: "6rem",
        fontSize: "calc(6rem / 2.5)",
      },
      "2xl": {
        size: "8rem",
        fontSize: "calc(8rem / 2.5)",
      },
      full: {
        size: "100%",
        fontSize: "calc(100% / 2.5)",
      },
    },
  })}
`;

export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
  (props, ref) => {
    const {
      src,
      name,
      s = "md",
      borderRadius = "9999px",
      showBorder,
      borderWidth,
      onError,
      getInitials = initials,
      icon = <AvatarFallback />,
      iconLabel = " avatar",
      loading,
      children,
      ...delegated
    } = props;

    return (
      <BaseAvatar
        ref={ref}
        s={s}
        name={name}
        borderWidth={showBorder ? "2px" : borderWidth}
        borderRadius={borderRadius}
        {...delegated}
      >
        <AvatarImage
          src={src}
          loading={loading}
          onError={onError}
          getInitials={getInitials}
          name={name}
          borderRadius={borderRadius}
          icon={icon}
          iconLabel={iconLabel}
        />
        {children}
      </BaseAvatar>
    );
  }
);
```

- The above code is self-explanatory if you are following this series. We created a `s` (size) variant. Handled the light and dark bg colors, we are creating a random color for each avatar.

### Story

- With the above our `Avatar` component is completed, let us create a story.
- Under the `src/components/atoms/avatar/avatar.stories.tsx` file we add the below story code.

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

import { Stack } from "../layout";
import { AiOutlineUser } from "../icons";
import { Avatar, AvatarBadge, AvatarProps } from "./avatar";

export default {
  title: "Atoms/Avatar",
};

export const Playground = {
  argTypes: {
    s: {
      name: "s",
      type: { name: "string", required: false },
      defaultValue: "md",
      description: "Size for the Avatar",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "md" },
      },
      control: {
        type: "select",
        options: ["xs", "2xs", "sm", "md", "lg", "xl", "2xl", "full"],
      },
    },
    name: {
      name: "size",
      type: { name: "string", required: false },
      defaultValue: "Segun Adebayo",
      description: `The name of the person in the avatar.
      -If src has loaded, the name will be used as the alt attribute of the img
      -If src is not loaded, the name will be used to create the initials`,
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "-" },
      },
    },
    src: {
      name: "src",
      type: { name: "string", required: false },
      defaultValue: "https://bit.ly/sage-adebayo",
      description: "The image url of the Avatar",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "-" },
      },
    },
  },
  render: (args: AvatarProps) => <Avatar {...args} />,
};

export const Default = {
  render: () => (
    <Stack direction="column" spacing="xl">
      <Stack>
        <Avatar src="https://bit.ly/broken-link" />
        <Avatar name="Ryan Florence" src="https://bit.ly/ryan-florence" />
        <Avatar name="Segun Adebayo" />
        <Avatar name="Kent Dodds" src="https://bit.ly/kent-c-dodds" />
        <Avatar name="Prosper Otemuyiwa" src="https://bit.ly/prosper-baba" />
        <Avatar name="Christian Nwamba" src="https://bit.ly/code-beast" />
      </Stack>
      <Stack>
        <Avatar>
          <AvatarBadge size="1.25em" bg="green500" />
        </Avatar>
        <Avatar>
          <AvatarBadge borderColor="papayawhip" bg="tomato" size="1.25em" />
        </Avatar>
      </Stack>
      <Stack>
        <Avatar bg="red500" icon={<AiOutlineUser fontSize="1.5rem" />} />
        <Avatar bg="teal500" />
      </Stack>
    </Stack>
  ),
};
```

### Build the Library

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

```ts
export * from "./avatar";
```

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

```tsx
export * from "./layout";
export * from "./typography";
export * from "./feedback";
export * from "./icon";
export * from "./icons";
export * from "./form";
export * from "./image";
export * from "./tag";
export * from "./badge";
export * from "./avatar";
```

- Now `npm run build`.

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

### Summary

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