Avatar
A picture, initials or an icon in a circle, on the same five sizes as every control so an avatar and a button line up.
split - size not measured yet
Avatar
split - size not measured yetA picture, initials or an icon in a circle, on the same five sizes as every control so an avatar and a button line up.
Try it
size
The code
avatar.tsx
import { AvatarFallback, AvatarImage, AvatarRoot } from 'topclassui/avatar'
<AvatarRoot>
<AvatarImage src={PORTRAIT} alt="Omar Abed" />
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>The fallback shows while the picture loads and after it fails; initials are its children.
Every state
5 statesInitials
Icon
Loading
Import from the package
topclassui/avatar
import { AvatarRoot, AvatarImage, AvatarFallback, AvatarSkeleton } from 'topclassui/avatar'Install once:npm i topclassui@beta zod
Examples
- Every part is a flat export:
AvatarRoot,AvatarImage,AvatarFallbackandAvatarSkeleton. There is noAvatar, and onlyAvatarRoottakessize. - Always render the fallback: the picture is laid over it, so it shows until the picture paints, with no JavaScript.
AvatarImageis the one client part. A picture that fails turns transparent and stays, so itsaltstill names the avatar, and the fallback shows.- An avatar of initials or an icon stays server HTML. Every avatar with a picture hydrates, so a table of fifty rows with pictures is fifty client parts.
- For next/image, pass it as
renderonAvatarImage; it carries its own picture and alt text.
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>
)
}
In Arabic, right to left
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>
)
}
Props
AvatarRoot
AvatarImage
AvatarFallback
AvatarSkeleton
Keyboard and accessibility
Nothing to operate: it is not interactive on its own.
AvatarImagerequiresalt, or arenderelement that carries its own. Passalt=""when the name is written beside the avatar, so it is not read twice.- The fallback renders
aria-hidden="true", so the picture'saltis the one name a screen reader reads. - With no picture and no name beside it, put
aria-labelonAvatarRoot: it then rendersrole="img"itself.
Right to left
The circle has no side to mirror; in a row, the avatar sits at the inline start and Arabic initials read right to left.
Server and client
Renders on the server; the interactive part is a client component that ships only when it is used.
Files: index.tsx, client.tsx.
On the chip: split - size not measured yet.