Avatar
A circular user image with an initials or icon fallback.
A circular user image with a graceful fallback. While the image is missing or fails to load, the avatar shows initials (or a projected icon), then swaps to the image once it loads. Sizes are sm / md / lg. A tcn-avatar-group wrapper overlaps its members and rings each one — the styling differs only subtly per platform (the ring tone) and is carried by the cascade.
Installation
npx touchcn add avatar
Usage
<tcn-avatar size="md" src="/avatar.jpg" alt="Avery" initials="AV" />
<tcn-avatar-group>
<tcn-avatar src="/one.jpg" alt="One" initials="1" />
<tcn-avatar src="/two.jpg" alt="Two" initials="2" />
<tcn-avatar initials="+5" />
</tcn-avatar-group>import { TcnAvatar, TcnAvatarGroup } from '@/components/ui/avatar';
<TcnAvatar size="md" src="/avatar.jpg" alt="Avery" initials="AV" />
<TcnAvatarGroup>
<TcnAvatar src="/one.jpg" alt="One" initials="1" />
<TcnAvatar src="/two.jpg" alt="Two" initials="2" />
<TcnAvatar initials="+5" />
</TcnAvatarGroup><script setup lang="ts">
import { TcnAvatar, TcnAvatarGroup } from '@/components/ui/avatar';
</script>
<template>
<TcnAvatar size="md" src="/avatar.jpg" alt="Avery" initials="AV" />
<TcnAvatarGroup>
<TcnAvatar src="/one.jpg" alt="One" initials="1" />
<TcnAvatar src="/two.jpg" alt="Two" initials="2" />
<TcnAvatar initials="+5" />
</TcnAvatarGroup>
</template>The only behaviour is a boolean load/error flag, so it stays in the copied component — no engine plumbing. Projected content (Angular ng-content, React children) renders as the fallback, so an icon can stand in for missing initials.
Props
Avatar
| Prop | Type | Default | Description |
|---|---|---|---|
src |
string |
'' |
Image URL; falls back on error or while empty. |
alt |
string |
'' |
Alt text for the image. |
initials |
string |
'' |
Fallback initials shown when there’s no image. |
size |
'sm' | 'md' | 'lg' |
'md' |
Avatar diameter. |
Source · Angular
export * from './tcn-avatar';import { Component, computed, ElementRef, inject, input, signal } from '@angular/core';
import { cn } from '@touchcn/core';
export type TcnAvatarSize = 'sm' | 'md' | 'lg';
/**
* Avatar — a circular user image with a graceful fallback. While the image is
* missing or fails to load it shows the `initials` (or a projected icon), then
* swaps to the image once it loads. Sizes are `sm` / `md` / `lg`; the styling
* differs only subtly per platform (the ring in a group, surface tones) and is
* carried by the cascade.
*
* The only behaviour is a boolean load/error flag — trivial enough to keep in
* the copied component (no engine plumbing).
*/
@Component({
selector: 'tcn-avatar',
template: `
<span class="tcn-avatar-fallback" aria-hidden="true">
{{ initials() }}<ng-content />
</span>
@if (src() && !failed()) {
<img
class="tcn-avatar-image"
[src]="src()"
[alt]="alt()"
(error)="failed.set(true)"
/>
}
`,
host: {
'[class]': 'classes()',
'[attr.data-size]': 'size()',
},
})
export class TcnAvatar {
private readonly authored = inject(ElementRef<HTMLElement>).nativeElement.getAttribute('class') ?? '';
readonly src = input('');
readonly alt = input('');
readonly initials = input('');
readonly size = input<TcnAvatarSize>('md');
protected readonly failed = signal(false);
protected readonly classes = computed(() => cn('tcn-avatar', this.authored));
}
/** Wraps overlapping avatars into a stacked group with an overlap ring. */
@Component({
selector: 'tcn-avatar-group',
template: '<ng-content />',
host: { class: 'tcn-avatar-group' },
})
export class TcnAvatarGroup {}Source · React
export * from './tcn-avatar';import { useState } from 'react';
import type { ReactNode } from 'react';
import { cn } from '@touchcn/core';
export type TcnAvatarSize = 'sm' | 'md' | 'lg';
export interface TcnAvatarProps {
src?: string;
alt?: string;
/** Fallback initials shown while the image is missing or failed. */
initials?: string;
size?: TcnAvatarSize;
/** Projected fallback (e.g. an icon) shown alongside/instead of initials. */
children?: ReactNode;
className?: string;
/** Accessible name when the avatar has no text (e.g. an icon fallback). */
'aria-label'?: string;
}
/**
* Avatar — a circular user image with a graceful fallback. While the image is
* missing or fails to load it shows the `initials` (or projected `children`),
* then swaps to the image once it loads. Sizes are `sm` / `md` / `lg`; the
* styling differs only subtly per platform (the ring in a group, surface tones)
* and is carried by the cascade.
*
* The only behaviour is a boolean error flag — trivial enough to keep in the
* copied component (no engine plumbing).
*/
export function TcnAvatar({
src,
alt = '',
initials,
size = 'md',
children,
className,
'aria-label': ariaLabel,
}: TcnAvatarProps) {
const [failed, setFailed] = useState(false);
return (
<span className={cn('tcn-avatar', className)} data-size={size} aria-label={ariaLabel} role={ariaLabel ? 'img' : undefined}>
<span className="tcn-avatar-fallback" aria-hidden="true">
{initials}
{children}
</span>
{src && !failed && (
<img className="tcn-avatar-image" src={src} alt={alt} onError={() => setFailed(true)} />
)}
</span>
);
}
export interface TcnAvatarGroupProps {
children?: ReactNode;
className?: string;
}
/** Wraps overlapping avatars into a stacked group with an overlap ring. */
export function TcnAvatarGroup({ children, className }: TcnAvatarGroupProps) {
return <span className={cn('tcn-avatar-group', className)}>{children}</span>;
}Source · Vue
<script lang="ts">
export type TcnAvatarSize = 'sm' | 'md' | 'lg';
</script>
<script setup lang="ts">
import { computed, ref, useAttrs } from 'vue';
/**
* Avatar — a circular user image with a graceful fallback. While the image is
* missing or fails to load it shows the `initials` (or projected slot content),
* then swaps to the image once it loads. Sizes are `sm` / `md` / `lg`; the
* styling differs only subtly per platform (the ring in a group, surface tones)
* and is carried by the cascade.
*
* The only behaviour is a boolean error flag — trivial enough to keep in the
* copied component (no engine plumbing).
*/
const props = withDefaults(
defineProps<{
src?: string;
alt?: string;
/** Fallback initials shown while the image is missing or failed. */
initials?: string;
size?: TcnAvatarSize;
}>(),
{ alt: '', size: 'md' },
);
const attrs = useAttrs();
const failed = ref(false);
// Give the avatar an image role only when it carries an accessible name.
const role = computed(() => (attrs['aria-label'] ? 'img' : undefined));
</script>
<template>
<span class="tcn-avatar" :data-size="size" :role="role">
<span class="tcn-avatar-fallback" aria-hidden="true">{{ initials }}<slot /></span>
<img
v-if="props.src && !failed"
class="tcn-avatar-image"
:src="props.src"
:alt="alt"
@error="failed = true"
/>
</span>
</template><script setup lang="ts">
/** Wraps overlapping avatars into a stacked group with an overlap ring. */
</script>
<template>
<span class="tcn-avatar-group"><slot /></span>
</template>export { default as TcnAvatar } from './TcnAvatar.vue';
export type { TcnAvatarSize } from './TcnAvatar.vue';
export { default as TcnAvatarGroup } from './TcnAvatarGroup.vue';