Avatar
Avatar is an image that represents a user or organization.
Installation
Install the following dependencies:
npm install @radix-ui/react-slotCreate an avatar.tsx file and paste the following code into it.
// CoreUI Avatar v0.0.0import * as React from 'react';import { Slot } from '@radix-ui/react-slot';import { IconEmptyCompany, IconEmptyUser,} from '@/components/ui/avatar-empty-icons';import { cn } from '@/utils/cn';import { recursiveCloneChildren } from '@/utils/recursive-clone-children';import { tv, type VariantProps } from '@/utils/tv';export const AVATAR_ROOT_NAME = 'AvatarRoot';const AVATAR_IMAGE_NAME = 'AvatarImage';const AVATAR_INDICATOR_NAME = 'AvatarIndicator';const AVATAR_STATUS_NAME = 'AvatarStatus';const AVATAR_BRAND_LOGO_NAME = 'AvatarBrandLogo';const AVATAR_NOTIFICATION_NAME = 'AvatarNotification';export const avatarVariants = tv({ slots: { root: [ 'relative flex shrink-0 items-center justify-center rounded-full', 'select-none text-center uppercase', ], image: 'size-full rounded-full object-cover', indicator: 'absolute flex size-8 items-center justify-center drop-shadow-[0_2px_4px_#1b1c1d0a]', }, variants: { size: { '80': { root: 'size-20 text-title-h5', }, '72': { root: 'size-18 text-title-h5', }, '64': { root: 'size-16 text-title-h5', }, '56': { root: 'size-14 text-label-lg', }, '48': { root: 'size-12 text-label-lg', }, '40': { root: 'size-10 text-label-md', }, '32': { root: 'size-8 text-label-sm', }, '24': { root: 'size-6 text-label-xs', }, '20': { root: 'size-5 text-label-xs', }, }, color: { gray: { root: 'bg-bg-soft-200 text-static-black', }, yellow: { root: 'bg-yellow-200 text-yellow-950', }, blue: { root: 'bg-blue-200 text-blue-950', }, sky: { root: 'bg-sky-200 text-sky-950', }, purple: { root: 'bg-purple-200 text-purple-950', }, red: { root: 'bg-red-200 text-red-950', }, }, }, compoundVariants: [ { size: ['80', '72'], class: { indicator: '-right-2', }, }, { size: '64', class: { indicator: '-right-2 scale-[.875]', }, }, { size: '56', class: { indicator: '-right-1.5 scale-75', }, }, { size: '48', class: { indicator: '-right-1.5 scale-[.625]', }, }, { size: '40', class: { indicator: '-right-1.5 scale-[.5625]', }, }, { size: '32', class: { indicator: '-right-1.5 scale-50', }, }, { size: '24', class: { indicator: '-right-1 scale-[.375]', }, }, { size: '20', class: { indicator: '-right-1 scale-[.3125]', }, }, ], defaultVariants: { size: '80', color: 'gray', },});type AvatarSharedProps = VariantProps<typeof avatarVariants>;export type AvatarRootProps = VariantProps<typeof avatarVariants> & React.HTMLAttributes<HTMLDivElement> & { asChild?: boolean; placeholderType?: 'user' | 'company'; };const AvatarRoot = React.forwardRef<HTMLDivElement, AvatarRootProps>( ( { asChild, children, size, color, className, placeholderType = 'user', ...rest }, forwardedRef, ) => { const uniqueId = React.useId(); const Component = asChild ? Slot : 'div'; const { root } = avatarVariants({ size, color }); const sharedProps: AvatarSharedProps = { size, color, }; // use placeholder icon if no children provided if (!children) { return ( <div className={root({ class: className })} {...rest}> <AvatarImage asChild> {placeholderType === 'company' ? ( <IconEmptyCompany /> ) : ( <IconEmptyUser /> )} </AvatarImage> </div> ); } const extendedChildren = recursiveCloneChildren( children as React.ReactElement[], sharedProps, [AVATAR_IMAGE_NAME, AVATAR_INDICATOR_NAME], uniqueId, asChild, ); return ( <Component ref={forwardedRef} className={root({ class: className })} {...rest} > {extendedChildren} </Component> ); },);AvatarRoot.displayName = AVATAR_ROOT_NAME;type AvatarImageProps = AvatarSharedProps & Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'color'> & { asChild?: boolean; };const AvatarImage = React.forwardRef<HTMLImageElement, AvatarImageProps>( ({ asChild, className, size, color, ...rest }, forwardedRef) => { const Component = asChild ? Slot : 'img'; const { image } = avatarVariants({ size, color }); return ( <Component ref={forwardedRef} className={image({ class: className })} {...rest} /> ); },);AvatarImage.displayName = AVATAR_IMAGE_NAME;function AvatarIndicator({ size, color, className, position = 'bottom', ...rest}: AvatarSharedProps & React.HTMLAttributes<HTMLDivElement> & { position?: 'top' | 'bottom'; }) { const { indicator } = avatarVariants({ size, color }); return ( <div className={cn(indicator({ class: className }), { 'top-0 origin-top-right': position === 'top', 'bottom-0 origin-bottom-right': position === 'bottom', })} {...rest} /> );}AvatarIndicator.displayName = AVATAR_INDICATOR_NAME;export const avatarStatusVariants = tv({ base: 'box-content size-3 rounded-full border-4 border-bg-white-0', variants: { status: { online: 'bg-success-base', offline: 'bg-faded-base', busy: 'bg-error-base', away: 'bg-away-base', }, }, defaultVariants: { status: 'online', },});function AvatarStatus({ status, className, ...rest}: VariantProps<typeof avatarStatusVariants> & React.HTMLAttributes<HTMLDivElement>) { return ( <div className={avatarStatusVariants({ status, class: className })} {...rest} /> );}AvatarStatus.displayName = AVATAR_STATUS_NAME;type AvatarBrandLogoProps = React.ImgHTMLAttributes<HTMLImageElement> & { asChild?: boolean;};const AvatarBrandLogo = React.forwardRef< HTMLImageElement, AvatarBrandLogoProps>(({ asChild, className, ...rest }, forwardedRef) => { const Component = asChild ? Slot : 'img'; return ( <Component ref={forwardedRef} className={cn( 'box-content size-6 rounded-full border-2 border-bg-white-0', className, )} {...rest} /> );});AvatarBrandLogo.displayName = AVATAR_BRAND_LOGO_NAME;function AvatarNotification({ className, ...rest}: React.HTMLAttributes<HTMLDivElement>) { return ( <div className={cn( 'box-content size-3 rounded-full border-2 border-bg-white-0 bg-error-base', className, )} {...rest} /> );}AvatarNotification.displayName = AVATAR_NOTIFICATION_NAME;export { AvatarRoot as Root, AvatarImage as Image, AvatarIndicator as Indicator, AvatarStatus as Status, AvatarBrandLogo as BrandLogo, AvatarNotification as Notification,};Create an avatar-empty-icons.tsx file and paste the following code into it.
// CoreUI Avatar Empty Icons v0.0.0'use client';import * as React from 'react';export function IconEmptyUser(props: React.SVGProps<SVGSVGElement>) { const clipPathId = React.useId(); return ( <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 80 80' {...props} > <g fill='#fff' clipPath={`url(#${clipPathId})`}> <ellipse cx={40} cy={78} fillOpacity={0.72} rx={32} ry={24} /> <circle cx={40} cy={32} r={16} opacity={0.9} /> </g> <defs> <clipPath id={clipPathId}> <rect width={80} height={80} fill='#fff' rx={40} /> </clipPath> </defs> </svg> );}export function IconEmptyCompany(props: React.SVGProps<SVGSVGElement>) { const clipPathId = React.useId(); const filterId1 = React.useId(); const filterId2 = React.useId(); return ( <svg xmlns='http://www.w3.org/2000/svg' width={56} height={56} fill='none' viewBox='0 0 56 56' {...props} > <g clipPath={`url(#${clipPathId})`}> <rect width={56} height={56} className='fill-bg-soft-200' rx={28} /> <path className='fill-bg-soft-200' d='M0 0h56v56H0z' /> <g filter={`url(#${filterId1})`} opacity={0.48}> <path fill='#fff' d='M7 24.9a2.8 2.8 0 012.8-2.8h21a2.8 2.8 0 012.8 2.8v49a2.8 2.8 0 01-2.8 2.8h-21A2.8 2.8 0 017 73.9v-49z' /> </g> <path className='fill-bg-soft-200' d='M12.6 28.7a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2z' /> <g filter={`url(#${filterId2})`}> <path fill='#fff' fillOpacity={0.8} d='M21 14a2.8 2.8 0 012.8-2.8h21a2.8 2.8 0 012.8 2.8v49a2.8 2.8 0 01-2.8 2.8h-21A2.8 2.8 0 0121 63V14z' /> </g> <path className='fill-bg-soft-200' d='M26.6 17.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7V22a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm9.8-29.4a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7V22a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2zm0 9.8a.7.7 0 01.7-.7h4.2a.7.7 0 01.7.7v4.2a.7.7 0 01-.7.7h-4.2a.7.7 0 01-.7-.7v-4.2z' /> </g> <defs> <filter id={filterId1} width={34.6} height={62.6} x={3} y={18.1} colorInterpolationFilters='sRGB' filterUnits='userSpaceOnUse' > <feFlood floodOpacity={0} result='BackgroundImageFix' /> <feGaussianBlur in='BackgroundImageFix' stdDeviation={2} /> <feComposite in2='SourceAlpha' operator='in' result='effect1_backgroundBlur_36237_4888' /> <feBlend in='SourceGraphic' in2='effect1_backgroundBlur_36237_4888' result='shape' /> </filter> <filter id={filterId2} width={42.6} height={70.6} x={13} y={3.2} colorInterpolationFilters='sRGB' filterUnits='userSpaceOnUse' > <feFlood floodOpacity={0} result='BackgroundImageFix' /> <feGaussianBlur in='BackgroundImageFix' stdDeviation={4} /> <feComposite in2='SourceAlpha' operator='in' result='effect1_backgroundBlur_36237_4888' /> <feBlend in='SourceGraphic' in2='effect1_backgroundBlur_36237_4888' result='shape' /> <feColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0' /> <feOffset dy={4} /> <feGaussianBlur stdDeviation={2} /> <feComposite in2='hardAlpha' k2={-1} k3={1} operator='arithmetic' /> <feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0' /> <feBlend in2='shape' result='effect2_innerShadow_36237_4888' /> </filter> <clipPath id={clipPathId}> <rect width={56} height={56} fill='#fff' rx={28} /> </clipPath> </defs> </svg> );}Examples
Color
Size
Text Content
Placeholder
Displays a placeholder icon if no children provided for <Avatar.Root>.
Status
Notification
With Brand Logo
Indicator With Custom SVG
As Link
As "next/image"
Composition
You can simplify component usage by creating a custom API that abstracts the core functionalities.
API Reference
Avatar.Root
The main wrapper for the avatar component. This component is based on the <div> element and supports all of its props and adds:
| Prop | Type | Default |
|---|---|---|
size | "80" | "72" | "64" | "56" | "48" | "40" | "32" | "24" | "20" | "80" |
color | "gray" | "yellow" | "blue" | "sky" | "purple" | "red" | |
placeholderType | "user" | "company" | "user" |
asChild | boolean |
Avatar.Image
This component is based on the <img> element and supports all of its props and adds:
| Prop | Type | Default |
|---|---|---|
asChild | boolean |
Avatar.BrandLogo
This component is based on the <img> element and supports all of its props and adds:
| Prop | Type | Default |
|---|---|---|
asChild | boolean |
Avatar.Indicator
A wrapper for absolutely positioned indicators, often used to show status or notifications. This component is based on the <div> element and supports all of its props and adds:
| Prop | Type | Default |
|---|---|---|
position | "top" | "bottom" |
Avatar.Status
Displays a status indicator, like online or offline status. This component is based on the <div> element and supports all of its props and adds:
| Prop | Type | Default |
|---|---|---|
status | "online" | "offline" | "busy" | "away" |
Avatar.Notification
Displays a notification badge on the avatar. This component is based on the <div> element and supports all of its props.