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
Under the
components/atomsfolder create a new folder calledavatar. Underavatarfolder create 3 filesavatar.tsx,avatar.stories.tsxandindex.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
- Under the
utilsfolder create a new filetheme.tsand 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();
}
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,AvatarBadgeandAvatarGroup. We will createAvatarGroupin the next tutorial. Internally we have another component calledAvatarName&AvatarImage.For the
AvatarBadgepaste 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}
/>
);
}
);
- We are extending
Boxwith 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>;
};
AvatarOptions Type
- Paste the following code before the
AvatarBadgecomponent -
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
srchas loaded, the name will be used as thealtattribute of theimg. Ifsrcis not loaded, the name will be used to create the initials. - showBorder - If
true, theAvatarwill 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, andsrcis not specified. - getInitials(name: string) - Function to get the initials to display
- name - The name of the person in the avatar. If
Did you find some image related props similar to the
Imagecomponent /useImagehook params we completed last tutorial, we will be using theuseImagehook forAvatarImage.
AvatarImage Component
- Paste the following code below
AvatarNamecomponent -
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
srcwas passed and the image has not loaded or failed to load. - If
srcwasn't passed.
- If
If
srcwas passed and the image has loaded, we'll return the Component.
Avatar Component
- Paste the Following code below the
AvatarImagecomponent -
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
Avatarcomponent is completed, let us create a story. - Under the
src/components/atoms/avatar/avatar.stories.tsxfile 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>
),
};
Build the Library
- Under the
avatar/index.tsfile paste the following -
export * from "./avatar";
- Under the
/atom/index.tsfile 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";
Now
npm run build.Under the folder
example/src/App.tsxwe can test ourAvatarcomponent. Copy paste the default story code and runnpm run startfrom theexampledirectory.
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.
Top comments (0)