DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI AvatarGroup 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 AvatarGroup component.

  • I would like you to first check the chakra docs for avatar.
  • All the code for this tutorial can be found under the atom-avatar branch here.

Prerequisite

Please check the previous post where we have completed the Avatar component. Also please check the Chakra Avatar Component code here. The theme / styles are here 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 -

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"> {}
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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%",
      },
    },
  })};
`;
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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

Build the Library

  • Under the avatar/index.ts file paste the following -
export * from "./avatar";
export * from "./avatar-group";
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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

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. In the next tutorial we will make our last component for this series Alert component. Until next time PEACE.

Discussion (0)