kanpeki
uiavatarsource: ok · hit
Avatar
npx shadcn add "https://kanpeki.vercel.app/r/avatar.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
@kanpeki/cva@kanpeki/custom-plugin
Files4 · source cached 10/8/2026, 5:55:32 PM
src/registry/ui/avatar/index.ts
export * from "./avatar";
export * as Avatar from "./namespace";
export * from "./styles";
src/registry/ui/avatar/avatar.tsx
"use client";
import { createContext, use, useLayoutEffect, useState } from "react";
import { AvatarStyles } from "./styles";
type Status = "idle" | "loading" | "success" | "error";
const AvatarContext = createContext<{
status: Status;
onStatusChange: (status: Status) => void;
} | null>(null);
function useAvatarContext() {
const context = use(AvatarContext);
if (!context) {
throw new Error("Avatar components must be rendered within the AvatarRoot");
}
return context;
}
export interface AvatarRootProps extends React.ComponentProps<"span"> {}
export function AvatarRoot({ className, ...props }: AvatarRootProps) {
const [status, setStatus] = useState<Status>("idle");
return (
<AvatarContext value={{ onStatusChange: setStatus, status }}>
<span
className={AvatarStyles.Root({ className })}
data-slot="avatar-root"
data-status={status}
{...props}
/>
</AvatarContext>
);
}
function useImageLoadingStatus(src?: string) {
const [status, setStatus] = useState<Status>("idle");
useLayoutEffect(() => {
if (!src) {
setStatus("error");
return;
}
let isMounted = true;
const image = new window.Image();
const updateStatus = (status: Status) => () => {
if (!isMounted) {
return;
}
setStatus(status);
};
setStatus("loading");
image.onload = updateStatus("success");
image.onerror = updateStatus("error");
image.src = src;
return () => {
isMounted = false;
};
}, [src]);
return status;
}
export interface AvatarImageProps extends React.ComponentProps<"img"> {
src?: string;
}
export function AvatarImage({
className,
src,
width,
height,
...props
}: AvatarImageProps) {
const context = useAvatarContext();
const status = useImageLoadingStatus(src);
useLayoutEffect(() => {
if (status !== "idle") {
context.onStatusChange(status);
}
}, [status, context.onStatusChange]);
if (status !== "success") {
return null;
}
return (
<img
{...props}
alt={props.alt}
className={AvatarStyles.Image({ className })}
data-slot="avatar-image"
height={height}
src={src}
width={width}
/>
);
}
export interface AvatarFallbackProps extends React.ComponentProps<"span"> {}
export function AvatarFallback({ className, ...props }: AvatarFallbackProps) {
const { status } = useAvatarContext();
return (
<span
{...props}
aria-hidden={status !== "error"}
className={AvatarStyles.Fallback({ className })}
data-slot="avatar-fallback"
/>
);
}
export interface AvatarPlaceholderProps extends React.ComponentProps<"span"> {}
export function AvatarPlaceholder({
className,
...props
}: AvatarPlaceholderProps) {
const { status } = useAvatarContext();
if (status !== "idle" && status !== "loading") {
return null;
}
return (
<span
{...props}
className={AvatarStyles.Placeholder({ className })}
data-slot="avatar-placeholder"
/>
);
}
src/registry/ui/avatar/styles.ts
import { cva } from "~/registry/lib/cva";
export const AvatarStyles = {
Fallback: cva({
base: [
"col-start-1 row-start-1 size-full select-none place-content-center rounded-full bg-muted",
"grid group-data-[status=success]:hidden",
],
}),
Image: cva({
base: ["aspect-square size-full"],
}),
Placeholder: cva({
base: [
"col-start-1 row-start-1 grid size-full animate-pulse place-content-center bg-muted",
],
}),
Root: cva({
base: ["group relative grid size-8 shrink-0 overflow-hidden rounded-full"],
}),
};
src/registry/ui/avatar/namespace.ts
export {
AvatarFallback as Fallback,
AvatarImage as Image,
AvatarPlaceholder as Placeholder,
AvatarRoot as Root,
} from "./avatar";