Build Chakra UI AvatarGroup component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-05 16:35:27

### Introduction

Let us continue building our chakra components using `styled-components` & `styled-system`. In this tutorial we will be cloning the Chakra UI `AvatarGroup` 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 `Avatar` component. 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 an AvatarGroup component.

### Setup

- We will continue working in the `atom-avatar` branch we created in the last tutorial.

- Under the `atoms/avatar` folder create a new file `avatar-group.tsx`

### AvatarGroup Component

- AvatarGroup displays a number of avatars grouped together in a stack.

- Let us first create types for our props -

```ts
import * as React from "react";
import styled from "styled-components";
import { variant, SpaceProps, ResponsiveValue } from "styled-system";

import { filterUndefined, getValidChildren } from "../../../utils";
import { Flex, FlexProps } from "../layout";
import { AvatarSizes } from "./avatar";

interface AvatarGroupOptions {
  spacing?: SpaceProps["margin"];
  max?: number;
  s?: ResponsiveValue<AvatarSizes>;
}

export interface AvatarGroupProps
  extends AvatarGroupOptions,
    Omit<FlexProps, "size"> {}
```

- We extend FlexProps and accept additional props like -

  - spacing - The space between the avatars in the group.
  - max - The maximum number of visible avatars in the group.
  - s - The size for the Avatars.

- Next we will create a new styled component called `ExcessLabel` which will display the number +3 or +2 -

```ts
const ExcessLabel = styled(Flex)<AvatarGroupProps>`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  text-transform: uppercase;
  font-weight: medium;
  position: relative;
  flex-shrink: 0;
  border-radius: 9999px;
  background-color: ${({ theme }) => theme.colors.gray200};
  ${variant({
    prop: "s",
    variants: {
      "2xs": {
        size: "1rem",
      },
      xs: {
        size: "1.5rem",
      },
      sm: {
        size: "2rem",
      },
      md: {
        size: "3rem",
      },
      lg: {
        size: "4rem",
      },
      xl: {
        size: "6rem",
      },
      "2xl": {
        size: "8rem",
      },
      full: {
        size: "100%",
      },
    },
  })};
`;
```

- Notice that we have a variant for the `Excess` label which same as the `Avatar` component so that the styles remain the same.

- Next let us create our AvatarGroup Component -

```tsx
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
  (props, ref) => {
    const {
      borderColor,
      max,
      spacing = "-0.75rem",
      borderRadius = "9999px",
      s = "md",
      children,
      ...delegated
    } = props;

    const validChildren = getValidChildren(children);

    const childrenWithinMax = max ? validChildren.slice(0, max) : validChildren;

    const excess = max != null && validChildren.length - max;

    const reversedChildren = childrenWithinMax.reverse();

    const clones = reversedChildren.map((child, index) => {
      const isFirstAvatar = index === 0;

      const childProps = {
        marginEnd: isFirstAvatar ? 0 : spacing,
        s,
        borderColor: child.props.borderColor ?? borderColor,
        showBorder: true,
      };

      return React.cloneElement(child, filterUndefined(childProps));
    });

    return (
      <Flex
        ref={ref}
        role="group"
        direction="row-reverse"
        align="center"
        justify="flex-end"
        {...delegated}
      >
        {excess > 0 && (
          <ExcessLabel
            s={s}
            borderRadius={borderRadius}
            marginStart={spacing}
          >{`+${excess}`}</ExcessLabel>
        )}
        {clones}
      </Flex>
    );
  }
);
```

- With the `childrenWithinMax` we get the avatars within the max.
- With `excess` we get the remaining avatar count.
- With `reverseChildren` - reversing the children is a great way to avoid using zIndex to overlap the avatars.

- Then we map over the visible avatars and apply the props, main thing here is marginEnd which is controlled by `spacing` prop passed to `AvatarGroup`. Also notice the `borderColor` it checks if that individual `Avatar` has a prop for borderColor if not then we apply the value passed to `AvatarGroup`.

- Also check to the `Flex` we passed direction = "row-reverse" because we reversed our children previously using `reverseChildren` function.

### Story

- With the above our `AvatarGroup` component is completed.
- Under the `src/components/atoms/avatar/avatar.stories.tsx` file we add the following for default story -

```tsx
<Stack>
  <AvatarGroup s="md" max={2}>
    <Avatar name="Ryan Florence" src="https://bit.ly/ryan-florence" />
    <Avatar name="Segun Adebayo" src="https://bit.ly/sage-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" />
  </AvatarGroup>
</Stack>
```

### Build the Library

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

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

- Now `npm run build`.

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

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

export function App() {
  return (
    <Stack m="1rem" 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>
        <AvatarGroup s="md" max={2}>
          <Avatar name="Ryan Florence" src="https://bit.ly/ryan-florence" />
          <Avatar name="Segun Adebayo" src="https://bit.ly/sage-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" />
        </AvatarGroup>
      </Stack>
    </Stack>
  );
}
```

### 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 make our last component for this series `Alert` component. Until next time PEACE.