Avatar
An image element with a fallback for representing the user.
npx shadcn@latest add @velaui/avatarpnpm dlx velaui add @velaui/avataryarn dlx shadcn@latest add @velaui/avatarbunx --bun shadcn@latest add @velaui/avatarveni add avatarInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/avatarpnpm dlx velaui add @velaui/avataryarn dlx shadcn@latest add @velaui/avatarbunx --bun shadcn@latest add @velaui/avatarExamples
- Photo
- Text solid
- Text subtle
- Icon
- Square
- Status
- Group circle
- Group square
Sizes
Usage
Props
Avatar
| Prop | Type | Default |
|---|---|---|
shape | "circle" | "square" | "circle" |
size | "sm" | "md" | "lg" | "2xs" | "xs" | "xl" | "2xl" | "md" |
tabIndex | number | 0 |
Takes these and every prop of AvatarProps.
AvatarImage
Takes every prop of AvatarImageProps.
AvatarFallback
| Prop | Type | Default |
|---|---|---|
background | "solid" | "subtle" | "subtle" |
color | "neutral" | "red" | "green" | "blue" | "yellow" | "neutral" |
Takes these and every prop of AvatarFallbackProps.
AvatarBadge
| Prop | Type | Default |
|---|---|---|
status | "online" | "away" | "busy" | "offline" | "online" |
Takes these and every prop of AvatarBadgeProps.
AvatarGroup
Takes every prop of AvatarGroupProps.
AvatarGroupCount
| Prop | Type | Default |
|---|---|---|
background | "solid" | "subtle" | "subtle" |
shape | "circle" | "square" | "circle" |
size | "sm" | "md" | "lg" | "2xs" | "xs" | "xl" | "2xl" | "md" |
Takes these and every prop of AvatarGroupCountProps.
Dependencies
- npm
class-variance-authority@0.7.1radix-ui@1.6.0- Installs with it
- nothing else
- npm
@base-ui/react@1.6.0class-variance-authority@0.7.1- Installs with it
- nothing else
Source
components/ui/avatar.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Avatar as AvatarPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const avatarVariants = cva(
"group/avatar relative flex shrink-0 font-semibold select-none outline-none",
{
variants: {
size: {
"2xs": "size-5 text-[8px]",
xs: "size-6 text-[10px]",
sm: "size-8 text-[13px]",
md: "size-10 text-[16px]",
lg: "size-12 text-[19px]",
xl: "size-14 text-[22px]",
"2xl": "size-16 text-[26px]",
},
shape: {
circle: "rounded-full",
square: "rounded-[22.5%]",
},
},
defaultVariants: {
size: "md",
shape: "circle",
},
},
);
export type AvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> &
VariantProps<typeof avatarVariants>;
function Avatar({
className,
size = "md",
shape = "circle",
tabIndex = 0,
...props
}: AvatarProps) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
data-shape={shape}
tabIndex={tabIndex}
className={cn(avatarVariants({ size, shape }), className)}
{...props}
/>
);
}
export type AvatarImageProps = React.ComponentProps<typeof AvatarPrimitive.Image>;
function AvatarImage({
className,
...props
}: AvatarImageProps) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-[inherit] border border-border-default object-cover",
"group-focus-visible/avatar:outline-2 group-focus-visible/avatar:outline-offset-1 group-focus-visible/avatar:outline-neutral-500/75",
className,
)}
{...props}
/>
);
}
const avatarFallbackVariants = cva(
"flex size-full items-center justify-center overflow-hidden rounded-[inherit] border border-border-default group-focus-visible/avatar:outline-2 group-focus-visible/avatar:outline-offset-1 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[55%]",
{
variants: {
background: {
subtle: "",
solid: "text-text-inverse",
},
color: {
neutral: "group-focus-visible/avatar:outline-neutral-500/75",
red: "group-focus-visible/avatar:outline-red-500/75",
green: "group-focus-visible/avatar:outline-green-500/75",
blue: "group-focus-visible/avatar:outline-blue-500/75",
yellow: "group-focus-visible/avatar:outline-yellow-500/75",
},
},
compoundVariants: [
// Subtle — the design tints the glyph a step darker than the initials,
// so the icon colour is not simply the text colour.
{
background: "subtle",
color: "neutral",
className: "bg-neutral-100 text-neutral-950 [&_svg]:text-neutral-700",
},
{
background: "subtle",
color: "red",
className: "bg-negative text-negative-strong [&_svg]:text-red-700",
},
{
background: "subtle",
color: "green",
className: "bg-success text-success-strong [&_svg]:text-green-700",
},
{
background: "subtle",
color: "blue",
className: "bg-blue-100 text-blue-700",
},
{
background: "subtle",
color: "yellow",
className: "bg-yellow-100 text-yellow-700",
},
{
background: "solid",
color: "neutral",
className: "bg-primary-default",
},
{ background: "solid", color: "red", className: "bg-negative-middle" },
{ background: "solid", color: "green", className: "bg-success-strong" },
{ background: "solid", color: "blue", className: "bg-blue-600" },
{ background: "solid", color: "yellow", className: "bg-yellow-700" },
],
defaultVariants: {
background: "subtle",
color: "neutral",
},
},
);
export type AvatarFallbackProps = React.ComponentProps<typeof AvatarPrimitive.Fallback> &
VariantProps<typeof avatarFallbackVariants>;
function AvatarFallback({
className,
background = "subtle",
color = "neutral",
...props
}: AvatarFallbackProps) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(avatarFallbackVariants({ background, color }), className)}
{...props}
/>
);
}
const avatarBadgeVariants = cva(
"absolute right-0 bottom-0 z-10 inline-flex size-[35%] items-center justify-center rounded-full border border-bg-main select-none",
{
variants: {
status: {
online: "bg-green-500",
away: "bg-amber-500",
busy: "bg-red-600",
offline: "bg-neutral-600",
},
},
defaultVariants: {
status: "online",
},
},
);
export type AvatarBadgeProps = React.ComponentProps<"span"> & VariantProps<typeof avatarBadgeVariants>;
function AvatarBadge({
className,
status = "online",
...props
}: AvatarBadgeProps) {
return (
<span
data-slot="avatar-badge"
className={cn(
avatarBadgeVariants({ status }),
"[&>svg]:size-2/3",
className,
)}
{...props}
/>
);
}
export type AvatarGroupProps = React.ComponentProps<"div">;
function AvatarGroup({ className, ...props }: AvatarGroupProps) {
return (
<div
data-slot="avatar-group"
className={cn(
// Overlap and the white separator ring both scale with the avatars the
// group happens to contain, so neither needs a prop of its own.
"group/avatar-group flex has-data-[size=2xs]:-space-x-2 has-data-[size=xs]:-space-x-2.5 has-data-[size=sm]:-space-x-[13px] has-data-[size=md]:-space-x-4 has-data-[size=lg]:-space-x-[19px] has-data-[size=xl]:-space-x-[22px] has-data-[size=2xl]:-space-x-[26px]",
className,
)}
{...props}
/>
);
}
const avatarGroupCountVariants = cva(
"relative flex shrink-0 items-center justify-center border border-neutral-200 font-medium select-none",
{
variants: {
size: {
"2xs": "size-5 text-[8px]",
xs: "size-6 text-[8px]",
sm: "size-8 text-[11px]",
md: "size-10 text-[14px]",
lg: "size-12 text-[16px]",
xl: "size-14 text-[19px]",
"2xl": "size-16 text-[22px]",
},
background: {
subtle: "bg-neutral-100 text-neutral-600",
solid: "bg-primary-default text-text-inverse",
},
shape: {
circle: "rounded-full",
square: "rounded-[22.5%]",
},
},
defaultVariants: {
size: "md",
background: "subtle",
shape: "circle",
},
},
);
export type AvatarGroupCountProps = React.ComponentProps<"div"> &
VariantProps<typeof avatarGroupCountVariants>;
function AvatarGroupCount({
className,
size = "md",
shape = "circle",
background = "subtle",
...props
}: AvatarGroupCountProps) {
return (
<div
data-slot="avatar-group-count"
data-size={size}
className={cn(
avatarGroupCountVariants({ size, background, shape }),
className,
)}
{...props}
/>
);
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarBadge,
AvatarGroup,
AvatarGroupCount,
avatarVariants,
avatarFallbackVariants,
avatarBadgeVariants,
avatarGroupCountVariants,
};components/ui/avatar.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const avatarVariants = cva(
"group/avatar relative flex shrink-0 font-semibold select-none outline-none",
{
variants: {
size: {
"2xs": "size-5 text-[8px]",
xs: "size-6 text-[10px]",
sm: "size-8 text-[13px]",
md: "size-10 text-[16px]",
lg: "size-12 text-[19px]",
xl: "size-14 text-[22px]",
"2xl": "size-16 text-[26px]",
},
shape: {
circle: "rounded-full",
square: "rounded-[22.5%]",
},
},
defaultVariants: {
size: "md",
shape: "circle",
},
},
);
export type AvatarProps = AvatarPrimitive.Root.Props & VariantProps<typeof avatarVariants>;
function Avatar({
className,
size = "md",
shape = "circle",
tabIndex = 0,
...props
}: AvatarProps) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
data-shape={shape}
tabIndex={tabIndex}
className={cn(avatarVariants({ size, shape }), className)}
{...props}
/>
);
}
export type AvatarImageProps = AvatarPrimitive.Image.Props;
function AvatarImage({
className,
...props
}: AvatarImageProps) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-[inherit] border border-border-default object-cover",
"group-focus-visible/avatar:outline-2 group-focus-visible/avatar:outline-offset-1 group-focus-visible/avatar:outline-neutral-500/75",
className,
)}
{...props}
/>
);
}
const avatarFallbackVariants = cva(
"flex size-full items-center justify-center overflow-hidden rounded-[inherit] border border-border-default group-focus-visible/avatar:outline-2 group-focus-visible/avatar:outline-offset-1 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[55%]",
{
variants: {
background: {
subtle: "",
solid: "text-text-inverse",
},
color: {
neutral: "group-focus-visible/avatar:outline-neutral-500/75",
red: "group-focus-visible/avatar:outline-red-500/75",
green: "group-focus-visible/avatar:outline-green-500/75",
blue: "group-focus-visible/avatar:outline-blue-500/75",
yellow: "group-focus-visible/avatar:outline-yellow-500/75",
},
},
compoundVariants: [
// Subtle — the design tints the glyph a step darker than the initials,
// so the icon colour is not simply the text colour.
{
background: "subtle",
color: "neutral",
className: "bg-neutral-100 text-neutral-950 [&_svg]:text-neutral-700",
},
{
background: "subtle",
color: "red",
className: "bg-negative text-negative-strong [&_svg]:text-red-700",
},
{
background: "subtle",
color: "green",
className: "bg-success text-success-strong [&_svg]:text-green-700",
},
{
background: "subtle",
color: "blue",
className: "bg-blue-100 text-blue-700",
},
{
background: "subtle",
color: "yellow",
className: "bg-yellow-100 text-yellow-700",
},
{
background: "solid",
color: "neutral",
className: "bg-primary-default",
},
{ background: "solid", color: "red", className: "bg-negative-middle" },
{ background: "solid", color: "green", className: "bg-success-strong" },
{ background: "solid", color: "blue", className: "bg-blue-600" },
{ background: "solid", color: "yellow", className: "bg-yellow-700" },
],
defaultVariants: {
background: "subtle",
color: "neutral",
},
},
);
export type AvatarFallbackProps = AvatarPrimitive.Fallback.Props &
VariantProps<typeof avatarFallbackVariants>;
function AvatarFallback({
className,
background = "subtle",
color = "neutral",
...props
}: AvatarFallbackProps) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(avatarFallbackVariants({ background, color }), className)}
{...props}
/>
);
}
const avatarBadgeVariants = cva(
"absolute right-0 bottom-0 z-10 inline-flex size-[35%] items-center justify-center rounded-full border border-bg-main select-none",
{
variants: {
status: {
online: "bg-green-500",
away: "bg-amber-500",
busy: "bg-red-600",
offline: "bg-neutral-600",
},
},
defaultVariants: {
status: "online",
},
},
);
export type AvatarBadgeProps = React.ComponentProps<"span"> & VariantProps<typeof avatarBadgeVariants>;
function AvatarBadge({
className,
status = "online",
...props
}: AvatarBadgeProps) {
return (
<span
data-slot="avatar-badge"
className={cn(
avatarBadgeVariants({ status }),
"[&>svg]:size-2/3",
className,
)}
{...props}
/>
);
}
export type AvatarGroupProps = React.ComponentProps<"div">;
function AvatarGroup({ className, ...props }: AvatarGroupProps) {
return (
<div
data-slot="avatar-group"
className={cn(
// Overlap and the white separator ring both scale with the avatars the
// group happens to contain, so neither needs a prop of its own.
"group/avatar-group flex has-data-[size=2xs]:-space-x-2 has-data-[size=xs]:-space-x-2.5 has-data-[size=sm]:-space-x-[13px] has-data-[size=md]:-space-x-4 has-data-[size=lg]:-space-x-[19px] has-data-[size=xl]:-space-x-[22px] has-data-[size=2xl]:-space-x-[26px]",
className,
)}
{...props}
/>
);
}
const avatarGroupCountVariants = cva(
"relative flex shrink-0 items-center justify-center border border-neutral-200 font-medium select-none",
{
variants: {
size: {
"2xs": "size-5 text-[8px]",
xs: "size-6 text-[8px]",
sm: "size-8 text-[11px]",
md: "size-10 text-[14px]",
lg: "size-12 text-[16px]",
xl: "size-14 text-[19px]",
"2xl": "size-16 text-[22px]",
},
background: {
subtle: "bg-neutral-100 text-neutral-600",
solid: "bg-primary-default text-text-inverse",
},
shape: {
circle: "rounded-full",
square: "rounded-[22.5%]",
},
},
defaultVariants: {
size: "md",
background: "subtle",
shape: "circle",
},
},
);
export type AvatarGroupCountProps = React.ComponentProps<"div"> &
VariantProps<typeof avatarGroupCountVariants>;
function AvatarGroupCount({
className,
size = "md",
shape = "circle",
background = "subtle",
...props
}: AvatarGroupCountProps) {
return (
<div
data-slot="avatar-group-count"
data-size={size}
className={cn(
avatarGroupCountVariants({ size, background, shape }),
className,
)}
{...props}
/>
);
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarBadge,
AvatarGroup,
AvatarGroupCount,
avatarVariants,
avatarFallbackVariants,
avatarBadgeVariants,
avatarGroupCountVariants,
};