liquefy-ui
uiliquid-avatarsource: ok · hit
LiquidAvatar
A circular avatar with image, initials and status dot, plus an overlapping group.
npx shadcn add "https://liquefy-ui.com/r/liquid-avatar.json"free
Where it goes
stats
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@base-ui/react@1.8.0
Registry dependencies
https://liquefy-ui.com/r/liquefy-ui.jsonhttps://liquefy-ui.com/r/styles-prop.json
Files1 · source cached 10/8/2026, 3:51:10 PM
registry/liquid-avatar.tsx
'use client'
import { Avatar } from '@base-ui/react/avatar'
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
import { useLiquidStyles, type LiquidStyleProps } from '@/lib/styles-prop'
// Tracking the image with a single `errored` flag missed the cases that matter:
// a cached image that is already complete before React attaches `onError`, and
// a `src` that changes to a broken one after a good one has loaded. Base UI
// tracks the loading status instead, and swaps the fallback in from that.
export type LiquidAvatarProps = HTMLAttributes<HTMLSpanElement> & LiquidStyleProps & {
alt?: string
fallback?: ReactNode
name?: string
size?: 'sm' | 'md' | 'lg' | 'xl'
src?: string
tint?: string
}
const getInitials = (name: string): string =>
name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join('')
export const LiquidAvatar = forwardRef<HTMLSpanElement, LiquidAvatarProps>(({
alt,
className,
fallback,
name,
size = 'md',
src,
style,
styles,
tint,
...props
}, ref) => {
const root = useLiquidStyles('lq-avatar', {
className,
style,
styles,
vars: tint ? { '--lq-avatar-tint': tint } : undefined,
})
return (
<Avatar.Root
className={root.className}
data-liquid-size={size}
ref={ref}
style={root.style}
title={name}
{...props}
>
{src && <Avatar.Image alt={alt ?? name ?? ''} src={src} />}
<Avatar.Fallback
aria-label={name}
className="lq-avatar__fallback"
role={name ? 'img' : undefined}
>
{fallback ?? (name ? getInitials(name) : '•')}
</Avatar.Fallback>
</Avatar.Root>
)
})
LiquidAvatar.displayName = 'LiquidAvatar'
export type LiquidAvatarGroupProps = HTMLAttributes<HTMLSpanElement> & LiquidStyleProps & {
children: ReactNode
max?: number
size?: LiquidAvatarProps['size']
total?: number
}
export const LiquidAvatarGroup = forwardRef<HTMLSpanElement, LiquidAvatarGroupProps>(({
children,
className,
max,
size = 'md',
style,
styles,
total,
...props
}, ref) => {
const items = Array.isArray(children) ? children : [children]
const visible = max ? items.slice(0, max) : items
const overflow = (total ?? items.length) - visible.length
const root = useLiquidStyles('lq-avatar-group', { className, style, styles })
return (
<span className={root.className} ref={ref} style={root.style} {...props}>
{visible}
{overflow > 0 && (
<span className="lq-avatar lq-avatar--overflow" data-liquid-size={size}>
<span className="lq-avatar__fallback">+{overflow}</span>
</span>
)}
</span>
)
})
LiquidAvatarGroup.displayName = 'LiquidAvatarGroup'