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 · lightRegistry 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';