CoreUI is in beta - APIs and components may change.
CoreUI
Base Components

Avatar

Avatar is an image that represents a user or organization.


Installation

Install the following dependencies:

npm install @radix-ui/react-slot

Create an avatar.tsx file and paste the following code into it.

components/ui/avatar.tsx
// 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.

components/ui/avatar-empty-icons.tsx
// 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

EW
EW
EW

Placeholder

Displays a placeholder icon if no children provided for <Avatar.Root>.

Status

Notification

Indicator With Custom SVG

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:

PropTypeDefault
size"80" | "72" | "64" | "56" | "48" | "40" | "32" | "24" | "20""80"
color"gray" | "yellow" | "blue" | "sky" | "purple" | "red"
placeholderType"user" | "company""user"
asChildboolean

Avatar.Image

This component is based on the <img> element and supports all of its props and adds:

PropTypeDefault
asChildboolean

This component is based on the <img> element and supports all of its props and adds:

PropTypeDefault
asChildboolean

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:

PropTypeDefault
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:

PropTypeDefault
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.

On this page