صورة رمزية
صورة أو أحرف أولى أو أيقونة داخل دائرة، بالأحجام الخمسة نفسها لكل عنصر تحكم، فتصطف الصورة الرمزية مع الزر في الصف نفسه.
split - الحجم لم يُقَس بعد
صورة رمزية
split - الحجم لم يُقَس بعدصورة أو أحرف أولى أو أيقونة داخل دائرة، بالأحجام الخمسة نفسها لكل عنصر تحكم، فتصطف الصورة الرمزية مع الزر في الصف نفسه.
جرّبه
size
الشيفرة
avatar.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
<AvatarRoot>
<AvatarImage src={PORTRAIT} alt="Omar Abed" />
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>يظهر البديل أثناء تحميل الصورة وبعد فشلها؛ والأحرف الأولى هي محتواه.
كل الحالات، بدون تمرير الفأرة
5 حالاتInitials
Icon
Loading
أو استورده من الحزمة
topclassui/avatar
import { AvatarRoot, AvatarImage, AvatarFallback, AvatarSkeleton } from 'topclassui/avatar'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- كل جزء مُصدَّر باسم مستقل:
AvatarRootوAvatarImageوAvatarFallbackوAvatarSkeleton. لا يوجدAvatar، ووحدهAvatarRootيأخذsize. - اعرض البديل دائمًا: الصورة توضع فوقه، فيظهر حتى تُرسم الصورة، بلا JavaScript.
AvatarImageهو جزء العميل الوحيد. الصورة التي تفشل تصبح شفافة وتبقى، فيظلaltيسمّي الصورة الرمزية ويظهر البديل.- الصورة الرمزية بالأحرف الأولى أو بأيقونة تبقى HTML من الخادم. وكل صورة رمزية فيها صورة تمر بالترطيب، فجدول من خمسين صفًا بالصور يعني خمسين جزءًا في المتصفح.
- لاستخدام next/image مرّره عبر
renderفيAvatarImage؛ فهو يحمل صورته ونصه البديل.
basic
basic.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
/** Inline, so the demo never reaches the network. */
const PORTRAIT = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23475569'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23e2e8f0'/%3E%3Cpath d='M6 40a14 14 0 0 1 28 0z' fill='%23e2e8f0'/%3E%3C/svg%3E"
/** The docs sample, named by its alt, then initials alone, named by aria-label on the root. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<AvatarRoot size="lg">
<AvatarImage src={PORTRAIT} alt="Omar Abed" />
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
<AvatarRoot size="lg" aria-label="Lina Haddad">
<AvatarFallback>LH</AvatarFallback>
</AvatarRoot>
</div>
)
}
with-name
Omar AbedMaintainer
with-name.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
/** Inline, so the demo never reaches the network. */
const PORTRAIT = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23475569'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23e2e8f0'/%3E%3Cpath d='M6 40a14 14 0 0 1 28 0z' fill='%23e2e8f0'/%3E%3C/svg%3E"
/** The name is written beside it, so the picture takes an empty alt and nothing is read twice. */
export default function Demo() {
return (
<div className="flex items-center gap-3">
<AvatarRoot>
<AvatarImage src={PORTRAIT} alt="" />
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
<div className="flex flex-col">
<span className="text-sm font-medium text-body">Omar Abed</span>
<span className="text-sm text-muted">Maintainer</span>
</div>
</div>
)
}
sizes
sizes.tsx
import { Button } from 'topclassui/button'
import { AvatarFallback, AvatarRoot } from 'topclassui/avatar'
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
/** Each avatar beside a Button of the same size: the two are the same height, so rows line up. */
export default function Demo() {
return (
<div className="flex flex-col gap-3">
{SIZES.map((size) => (
<div key={size} className="flex items-center gap-3">
<AvatarRoot size={size} aria-label="Omar Abed">
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
<Button size={size} variant="outline" tone="neutral">Follow</Button>
</div>
))}
</div>
)
}
icon
icon.tsx
import { Svg } from 'topclassui/svg'
import { user } from 'topclassui/icons/user'
import { AvatarFallback, AvatarRoot } from 'topclassui/avatar'
/** An icon as the fallback, for an account with no name yet. The root carries the name. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<AvatarRoot size="sm" aria-label="Guest">
<AvatarFallback><Svg icon={user} /></AvatarFallback>
</AvatarRoot>
<AvatarRoot aria-label="Guest">
<AvatarFallback><Svg icon={user} /></AvatarFallback>
</AvatarRoot>
<AvatarRoot size="xl" aria-label="Guest">
<AvatarFallback><Svg icon={user} /></AvatarFallback>
</AvatarRoot>
</div>
)
}
failed
failed.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
/** Not a picture, so it fails the way a 404 does and the demo never reaches the network. */
const FAILED = 'data:image/svg+xml,failed'
/** A picture that failed: the browser's mark sits in the corner and the initials stay readable. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
{SIZES.map((size) => (
<AvatarRoot key={size} size={size}>
<AvatarImage src={FAILED} alt="Omar Abed" />
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
))}
</div>
)
}
skeleton
skeleton.tsx
import { AvatarFallback, AvatarRoot, AvatarSkeleton } from 'topclassui/avatar'
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
/** Each skeleton above the avatar it holds the place of: the same box at every size. */
export default function Demo() {
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
{SIZES.map((size) => <AvatarSkeleton key={size} size={size} />)}
</div>
<div className="flex items-center gap-3">
{SIZES.map((size) => (
<AvatarRoot key={size} size={size} aria-label="Omar Abed">
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
))}
</div>
</div>
)
}
custom-size
custom-size.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
/** Inline, so the demo never reaches the network. */
const PORTRAIT = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23475569'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23e2e8f0'/%3E%3Cpath d='M6 40a14 14 0 0 1 28 0z' fill='%23e2e8f0'/%3E%3C/svg%3E"
/** size="custom" emits no size classes, so className gives the box and the type step. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<AvatarRoot size="custom" className="size-24 text-lg">
<AvatarImage src={PORTRAIT} alt="Omar Abed" />
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
<AvatarRoot size="custom" className="size-24 text-lg" aria-label="Omar Abed">
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
</div>
)
}
بالعربية، من اليمين إلى اليسار
rtl.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
/** Inline, so the demo never reaches the network. */
const PORTRAIT = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23475569'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23e2e8f0'/%3E%3Cpath d='M6 40a14 14 0 0 1 28 0z' fill='%23e2e8f0'/%3E%3C/svg%3E"
export default function Demo() {
return (
<div dir="rtl" lang="ar" className="flex items-center gap-3">
<AvatarRoot size="lg">
<AvatarImage src={PORTRAIT} alt="" />
<AvatarFallback>ع ف</AvatarFallback>
</AvatarRoot>
<AvatarRoot size="lg">
<AvatarFallback>ل ح</AvatarFallback>
</AvatarRoot>
<span className="text-sm text-body">عمر فتحي وليلى حسن</span>
</div>
)
}
الخصائص
AvatarRoot
AvatarImage
AvatarFallback
AvatarSkeleton
لوحة المفاتيح وإمكانية الوصول
لا شيء يُشغَّل: ليس تفاعليًا بمفرده.
- يتطلب
AvatarImageقيمةalt، أو عنصرrenderيحمل نصه البديل. مرّرalt=""عندما يكون الاسم مكتوبًا بجانب الصورة الرمزية، كي لا يُقرأ مرتين. - يُعرض البديل مع
aria-hidden="true"، فيكونaltالصورة هو الاسم الوحيد الذي يقرؤه قارئ الشاشة. - إن لم تكن هناك صورة ولا اسم بجانبها، ضع
aria-labelعلىAvatarRoot: عندها يضيفrole="img"بنفسه.
من اليمين إلى اليسار
لا جهة في الدائرة تنعكس؛ في الصف تأتي الصورة الرمزية عند بداية السطر، والأحرف الأولى العربية تُقرأ من اليمين إلى اليسار.
الخادم والمتصفح
يُعرض على الخادم، والجزء التفاعلي مكوّن متصفح لا يُرسل إلا عند استخدامه.
الملفات: index.tsx, client.tsx.
على الشارة: split - الحجم لم يُقَس بعد.