shadimage
Awesome Cursor Rules
uiavatarsource: ok · refreshed

Avatar

User avatar primitives with image fallback for Tile UI.

npx shadcn add "https://react.tileui.zmorg.cn/r/avatar.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light

Registry dependencies

@tile-ui/core@tile-ui/styles

Files3 · source cached 10/2/2026, 10:25:23 AM

packages/react/src/components/avatar/avatar.tsx
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import { avatarStyleKeys, shouldShowAvatarFallback } from '../lib/core';
import type { AvatarImageStatus } from '../lib/core';
import type { AvatarBaseProps, AvatarImageBaseProps } from '../lib/core';
import styles from './avatar.module.scss';

interface AvatarContextValue {
	status: AvatarImageStatus;
	setStatus: (status: AvatarImageStatus) => void;
	hasImage: boolean;
	setHasImage: (has: boolean) => void;
}

const AvatarContext = createContext<AvatarContextValue | null>(null);

function useAvatarContext(): AvatarContextValue {
	const context = useContext(AvatarContext);
	if (!context) {
		throw new Error('Avatar sub-components must be used within <Avatar>.');
	}
	return context;
}

export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement>, AvatarBaseProps {}

const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(({ className = '', size = 'default', children, ...props }, ref) => {
	const [status, setStatus] = useState<AvatarImageStatus>('loading');
	const [hasImage, setHasImage] = useState(false);

	return (
		<AvatarContext.Provider value={{ status, setStatus, hasImage, setHasImage }}>
			<span ref={ref} data-size={size} className={`${styles[avatarStyleKeys.root]} ${className}`} {...props}>
				{children}
			</span>
		</AvatarContext.Provider>
	);
});
Avatar.displayName = 'Avatar';

export interface AvatarImageProps extends React.ImgHTMLAttributes<HTMLImageElement>, AvatarImageBaseProps {}

const AvatarImage = React.forwardRef<HTMLImageElement, AvatarImageProps>(({ className = '', onLoad, onError, ...props }, ref) => {
	const context = useAvatarContext();
	const imgRef = useRef<HTMLImageElement | null>(null);

	useEffect(() => {
		context.setHasImage(true);

		const img = imgRef.current;
		if (img && img.complete) {
			context.setStatus(img.naturalWidth > 0 ? 'loaded' : 'error');
		}
	}, [context]);

	function setRef(element: HTMLImageElement | null) {
		imgRef.current = element;

		if (typeof ref === 'function') {
			ref(element);
		} else if (ref) {
			ref.current = element;
		}
	}

	function handleLoad(event: React.SyntheticEvent<HTMLImageElement>) {
		context.setStatus('loaded');
		onLoad?.(event);
	}

	function handleError(event: React.SyntheticEvent<HTMLImageElement>) {
		context.setStatus('error');
		onError?.(event);
	}

	return <img ref={setRef} className={`${styles[avatarStyleKeys.image]} ${className}`} onLoad={handleLoad} onError={handleError} {...props} />;
});
AvatarImage.displayName = 'AvatarImage';

export interface AvatarFallbackProps extends React.HTMLAttributes<HTMLSpanElement> {}

const AvatarFallback = React.forwardRef<HTMLSpanElement, AvatarFallbackProps>(({ className = '', children, ...props }, ref) => {
	const context = useAvatarContext();

	if (!shouldShowAvatarFallback(context.status, context.hasImage)) {
		return null;
	}

	return (
		<span ref={ref} className={`${styles[avatarStyleKeys.fallback]} ${className}`} {...props}>
			{children}
		</span>
	);
});
AvatarFallback.displayName = 'AvatarFallback';

export interface AvatarBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {}

const AvatarBadge = React.forwardRef<HTMLSpanElement, AvatarBadgeProps>(({ className = '', children, ...props }, ref) => {
	return (
		<span ref={ref} className={`${styles[avatarStyleKeys.badge]} ${className}`} {...props}>
			{children}
		</span>
	);
});
AvatarBadge.displayName = 'AvatarBadge';

export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {}

const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(({ className = '', children, ...props }, ref) => {
	return (
		<div ref={ref} className={`${styles[avatarStyleKeys.group]} ${className}`} {...props}>
			{children}
		</div>
	);
});
AvatarGroup.displayName = 'AvatarGroup';

export interface AvatarGroupCountProps extends React.HTMLAttributes<HTMLDivElement> {}

const AvatarGroupCount = React.forwardRef<HTMLDivElement, AvatarGroupCountProps>(({ className = '', children, ...props }, ref) => {
	return (
		<div ref={ref} className={`${styles[avatarStyleKeys.groupCount]} ${className}`} {...props}>
			{children}
		</div>
	);
});
AvatarGroupCount.displayName = 'AvatarGroupCount';

export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount };
export default Avatar;
packages/styles/scss/components/avatar.module.scss
@use '../../../styles/variables/colors' as *;

.root {
	position: relative;
	display: inline-flex;
	flex-shrink: 0;
	width: 2rem;
	height: 2rem;
	overflow: hidden;
	border-radius: $radius-full;
	user-select: none;

	&[data-size='sm'] {
		width: 1.5rem;
		height: 1.5rem;
	}

	&[data-size='lg'] {
		width: 2.5rem;
		height: 2.5rem;
	}
}

.image {
	width: 100%;
	height: 100%;
	aspect-ratio: 1 / 1;
	object-fit: cover;
}

.fallback {
	display: flex;
	width: 100%;
	height: 100%;
	align-items: center;
	justify-content: center;
	border-radius: $radius-full;
	background-color: $muted;
	color: $muted-foreground;
	font-size: $text-sm;

	&[hidden] {
		display: none;
	}

	[data-size='sm'] & {
		font-size: $text-xs;
	}
}

.badge {
	position: absolute;
	right: 0;
	bottom: 0;
	z-index: 10;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: $radius-full;
	background-color: $primary;
	color: $primary-foreground;
	box-shadow: 0 0 0 2px $background;
	user-select: none;

	[data-size='sm'] & {
		width: 0.5rem;
		height: 0.5rem;

		svg {
			display: none;
		}
	}

	[data-size='default'] & {
		width: 0.625rem;
		height: 0.625rem;

		svg {
			width: 0.5rem;
			height: 0.5rem;
		}
	}

	[data-size='lg'] & {
		width: 0.75rem;
		height: 0.75rem;

		svg {
			width: 0.5rem;
			height: 0.5rem;
		}
	}
}

.group {
	display: flex;

	> .root {
		margin-left: -0.5rem;
		box-shadow: 0 0 0 2px $background;

		&:first-child {
			margin-left: 0;
		}
	}
}

.groupCount {
	position: relative;
	display: flex;
	flex-shrink: 0;
	width: 2rem;
	height: 2rem;
	align-items: center;
	justify-content: center;
	border-radius: $radius-full;
	background-color: $muted;
	color: $muted-foreground;
	font-size: $text-sm;
	box-shadow: 0 0 0 2px $background;

	svg {
		width: 1rem;
		height: 1rem;
	}
}
packages/react/src/components/avatar/index.ts
export {
	Avatar,
	AvatarImage,
	AvatarFallback,
	AvatarBadge,
	AvatarGroup,
	AvatarGroupCount,
	type AvatarProps,
	type AvatarImageProps,
	type AvatarFallbackProps,
	type AvatarBadgeProps,
	type AvatarGroupProps,
	type AvatarGroupCountProps,
} from './avatar';

More from Awesome Cursor Rules

Browse all components in this library →