DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI Avatar 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 Avatar 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 Image component and useImage hook. Also please check the Chakra Avatar Component code here. The theme / styles are here 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 -
git checkout -b atom-avatar
Enter fullscreen mode Exit fullscreen mode
  • 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 -
npm install tinycolor2
npm install --save-dev @types/tinycolor2
Enter fullscreen mode Exit fullscreen mode
  • Under the utils folder create a new file theme.ts and paste the following code -
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();
}
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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}
      />
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • We are extending Box with some default styles and passing some styles as props with default values.

AvatarName Component

  • Paste the following code -
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>;
};
Enter fullscreen mode Exit fullscreen mode

AvatarOptions Type

  • Paste the following code before the AvatarBadge component -
interface AvatarOptions {
  name?: string;
  showBorder?: boolean;
  src?: string;
  srcSet?: string;
  loading?: "eager" | "lazy";
  onError?: () => void;
  icon?: React.ReactElement;
  getInitials?: (name: string) => string;
}
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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"}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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 Component.

Avatar Component

  • Paste the Following code below the AvatarImage component -
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>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • 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.
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode

Build the Library

  • Under the avatar/index.ts file paste the following -
export * from "./avatar";
Enter fullscreen mode Exit fullscreen mode
  • Under the /atom/index.ts file paste the following -
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";
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 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. In the next tutorial we will create AvatarGroup component. Until next time PEACE.

Discussion (0)