Avatar | CoUI
LogoCoUI

Avatar

사용자 아바타 컴포넌트

Avatar#

사용자 프로필 이미지 또는 이니셜을 표시하는 아바타 컴포넌트입니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 사용자 프로필 이미지 또는 이니셜을 표시할 때
  • 댓글, 채팅, 팀 멤버 목록에서 사용자를 시각적으로 식별할 때
  • 온라인/오프라인 상태를 함께 표시해야 할 때

대신 다른 컴포넌트를 사용하세요:

  • Icon: 사용자가 아닌 일반 아이콘을 표시할 때
  • Badge: 텍스트 레이블이 필요한 상태 표시일 때
  • Image: 프로필이 아닌 일반 이미지를 표시할 때

기본 사용법 (Basic Usage)#

// 이미지 아바타
Avatar(
  imageUrl: 'https://example.com/photo.jpg',
  size: CoreComponentSize.md,
)

// 이니셜 아바타
Avatar(
  initials: '홍',
  size: CoreComponentSize.lg,
)

// 링 효과
Avatar(
  imageUrl: user.photoUrl,
  ring: true,
)

// 커스텀 child
Avatar(
  size: CoreComponentSize.sm,
  child: Icon(LucideIcons.user),
)
// 이미지 아바타
Avatar(
  imageUrl: 'https://example.com/photo.jpg',
  size: CoreComponentSize.md,
)

// 이니셜 아바타
Avatar(
  initials: '홍',
  size: CoreComponentSize.lg,
)

// 링 효과
Avatar(
  imageUrl: 'https://example.com/photo.jpg',
  size: CoreComponentSize.md,
  ring: true,
)

// 커스텀 child
Avatar(
  size: CoreComponentSize.sm,
  child: Text('JD').labelSmall.onSurface,
)

Props / Parameters#

속성타입기본값설명
imageUrl String? null 프로필 이미지 URL
initials String? null 이니셜 텍스트
size CoreComponentSize md 아바타 크기
ringboolfalse링 보더 표시 여부
child Widget? (Flutter) / Component? (Web) null 커스텀 내용 (imageUrl/initials 대체)
badge Widget? (Flutter) / Component? (Web) null 아바타 위에 오버레이 되는 배지 (AvatarBadge)
avatarStyle CoreAvatarStyle? null 인스턴스 스타일 (Style 시스템 참조)

badge 의 위치(badgeAlignment)·인셋(badgeInset) 등 모든 시각 chrome 은 avatarStyle: CoreAvatarStyle(badgeAlignment: ..., badgeInset: ...) 로 전달합니다.

스타일 시스템 (Style System)#

Avatar 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreAvatarStyle 단일 슬롯으로 흐릅니다. 시맨틱 enum (size) 과 behaviour (ring / badge / imageUrl / initials) 는 위젯 파라미터로 직접 전달합니다.

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯 파라미터로 직접 (size, ring, imageUrl, initials, child, badge)
  • chrome / dimensional / 슬롯 스타일: CoreAvatarStyle 한 곳으로 (backgroundColor / borderRadius / ringColor / ringWidth / ringOffset / badgeInset / badgeAlignment / initialsFontRatio / fallbackTextStyle). 이니셜 텍스트 색은 fallbackTextStyle.color 로 흐릅니다

Resolve chain#

design system default for avatar
  → CoreAvatarTheme.style                  // 프로젝트 공통
  → widget.avatarStyle                     // 인스턴스별

fallbackTextStyle 슬롯은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.

CoreAvatarStyle 필드#

필드타입설명
backgroundColor CoreColor? Avatar fill background colour. Drives both the image placeholder and the initials fallback. null → [defaultBackgroundColor].
borderRadius CoreBorderRadius? Border radius. null → [defaultBorderRadius] (circular).
ringColor CoreColor? Ring (outer border) colour when the widget's ring parameter is true . null → [defaultRingColor].
ringWidth double? Ring (outer border) width (logical px) when the widget's ring parameter is true . null → [defaultRingWidth].
ringOffset double? Ring offset (gap between ring and avatar fill, logical px) when the widget's ring parameter is true . null → [defaultRingOffset].
badgeInset double? Inset from the avatar corner to the badge (logical px, pre-scaling). null → [defaultBadgeInset].
badgeAlignment CoreAlignment? Corner the badge is overlaid at, relative to the avatar. Platform-neutral [CoreAlignment] — Flutter maps it to an AlignmentGeometry , Web maps it to CSS edge offsets. null → [defaultBadgeAlignment] (top-end).
initialsFontRatio double? Ratio of the initials fallback fontSize to the avatar diameter — a single-axis behaviour scalar ( fontSize = diameter * ratio ). null → [defaultInitialsFontRatio] (0.4).
fallbackTextStyle CoreTextStyle? Initials fallback text style (rendered when neither image nor custom child is supplied). Text colour is carried via [CoreTextStyle.color] inside this slot. null → [defaultFallbackTextStyle].

관련 컴포넌트의 Style 슬롯#

컴포넌트슬롯담는 것
AvatarBadge avatarBadgeStyle: CoreAvatarBadgeStyle? 배지 자체의 배경(backgroundColor) · 크기(size) — 이 두 필드뿐
AvatarGroup avatarGroupStyle: CoreAvatarGroupStyle? 겹침 간격( overlap / extraSpacing ) + 겹친 아바타를 갈라 보이게 하는 구분 보더( borderColor / borderWidth )

사용 예 (Flutter)#

Avatar(
  initials: 'JD',
  ring: true,
  avatarStyle: CoreAvatarStyle(
    backgroundColor: CoreColor.token(CoreColors.primaryContainer),
    ringColor: CoreColor.token(CoreColors.primary),
    ringWidth: CoreStrokeWidth.stroke3,
    fallbackTextStyle: CoreTextStyle.token(
      CoreTextStyles.labelLarge,
      color: CoreColor.token(CoreColors.onPrimaryContainer),
    ),
  ),
)

사용 예 (Web)#

Avatar(
  initials: 'JD',
  ring: true,
  avatarStyle: CoreAvatarStyle(
    backgroundColor: CoreColor.token(CoreColors.primaryContainer),
    fallbackTextStyle: CoreTextStyle.token(
      CoreTextStyles.labelLarge,
      color: CoreColor.token(CoreColors.onPrimaryContainer),
    ),
  ),
)

빠른 오버라이드 (Chain)#

이미 만든 Avatar 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class AvatarChainExample extends StatelessWidget {
  const AvatarChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        const Avatar(initials: 'AB').radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(ringColor/ringWidth)까지.
        const Avatar(initials: 'AB').withStyle(
          const CoreAvatarStyle(
            backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            ringColor: CoreColor.token(CoreColors.tertiary),
            ringWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class AvatarChainExample extends StatelessComponent {
  const AvatarChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        Avatar(initials: 'AB').radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(ringColor/ringWidth)까지.
        Avatar(initials: 'AB').withStyle(
          const CoreAvatarStyle(
            backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            ringColor: CoreColor.token(CoreColors.tertiary),
            ringWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

변형 (Variants)#

크기#

크기픽셀
XSCoreComponentSize.xs24px
SMCoreComponentSize.sm32px
MDCoreComponentSize.md40px
LGCoreComponentSize.lg48px
XLCoreComponentSize.xl64px
XL2CoreComponentSize.xl296px

링 효과#

Avatar(imageUrl: url, ring: true)
Avatar(imageUrl: url, ring: false)

배경색 커스텀#

배경색은 위젯 prop 이 아니라 avatarStyle 슬롯으로 흐릅니다.

Avatar(
  initials: 'AB',
  avatarStyle: CoreAvatarStyle(
    backgroundColor: CoreColor.token(CoreColors.primaryContainer),
  ),
)
Avatar(
  initials: 'CD',
  avatarStyle: CoreAvatarStyle(
    backgroundColor: CoreColor.token(CoreColors.secondaryContainer),
  ),
)

동작 스펙 (Behavior)#

이미지 로딩#

  • imageUrl 이 있으면 이미지를 그리고, 없으면 initials 로 폴백합니다
  • Flutter 는 네트워크 이미지, Web 은 <img> 태그로 렌더합니다
  • Web: <img>alt 와 루트 aria-labelinitials(없으면 avatar)가 자동 적용됩니다

Ring 효과#

  • 양 플랫폼 모두 ring: true 로 활성화하고, 두께와 색은 avatarStyleringWidth / ringColor 가 정합니다
  • 기본값은 CoreStrokeWidth.stroke2 두께 · primary
  • ringOffset(링과 아바타 사이 간격)은 현재 Web 만 적용합니다 — Flutter 위젯은 링을 BoxDecoration 보더로 그려서 그 값을 읽지 않습니다
Avatar(
  imageUrl: 'https://example.com/photo.jpg',
  ring: true,
)

이미지 폴백#

  • imageUrlinitials를 함께 제공하면 이미지 로드 실패 시 이니셜로 폴백
  • child가 있으면 imageUrl/initials보다 우선
Avatar(
  imageUrl: 'https://example.com/photo.jpg',
  initials: 'JD',
  size: CoreComponentSize.md,
)

사용 가이드라인 (Usage Guidelines)#

✅ Do#

이미지와 이니셜 폴백을 항상 함께 제공하세요.

Avatar(
  imageUrl: user.photoUrl,
  initials: 'JD',
)

이미지 로드 실패 시 이니셜이 표시되어 사용자를 식별할 수 있습니다.


❌ Don't#

이미지 없이 빈 아바타를 표시하지 마세요.

이니셜이나 기본 아이콘 없는 빈 원형은 사용자를 식별할 수 없습니다.

✅ Do#

그룹에는 AvatarGroupmax 를 설정하세요.

AvatarGroup(
  max: 5,
  children: teamMembers.map((m) => Avatar(
    imageUrl: m.photo,
    size: CoreComponentSize.sm,
  )).toList(),
)

많은 아바타가 나열되면 레이아웃이 깨집니다. 3~5개가 적절합니다.


❌ Don't#

아바타 크기를 컨텍스트에 맞지 않게 사용하지 마세요.

댓글 목록에 XL(64px) 아바타를 사용하면 콘텐츠보다 아바타가 강조됩니다. 인라인에는 SM(32px), 프로필 헤더에는 LG(48px) 이상을 사용하세요.

✅ Do#

상태 표시는 실시간 정보에만 사용하세요.

Avatar(
  imageUrl: user.photoUrl,
  ring: user.isOnline,
)

온라인/오프라인 같은 실시간 상태만 뱃지로 표시합니다.


❌ Don't#

정적 역할 정보를 아바타 뱃지로 표시하지 마세요.

"관리자", "멤버" 같은 역할은 별도 텍스트 레이블로 표시하세요.

접근성 (Accessibility)#

스크린 리더#

  • Flutter: 이니셜 폴백은 텍스트가 그대로 읽힙니다
  • Web: 루트 aria-label<img alt>initials(없으면 avatar)가 들어갑니다. 사용자 이름을 읽히려면 initials 에 의미 있는 값을 넣습니다

최소 크기#

  • 인터랙티브 아바타(클릭 가능): 최소 24×24 논리 픽셀 터치 영역 확보 (WCAG 2.2 AA, CoreTouchTarget.minimum) — 보이는 크기를 유지한 채 히트 영역만 넓힙니다
  • XS(24px) 아바타는 표시 전용으로만 사용

색상 대비#

  • 이니셜 텍스트와 배경색의 대비 WCAG AA 준수
  • Ring 효과는 색상 이외의 시각적 구분(간격)도 제공

크로스 플랫폼 차이점 (Platform Differences)#

imageUrl / initials / size / ring / child / badge / avatarStyle 은 양 플랫폼 동일 API 입니다.

항목FlutterWeb
슬롯 타입Widget?Component?
이미지 렌더네트워크 이미지 위젯<img src>
링 렌더 BoxDecoration 보더 (간격 미적용) CSS ring 유틸리티 (ringOffset 적용)
그룹 오버랩 RowTransform.translate 로 겹침 음수 margin 으로 겹침
  • Badge: 상태 텍스트 레이블. Avatar의 badge 대신 텍스트 기반 상태 표시
  • Icon: 아이콘 시스템. Avatar의 커스텀 child로 아이콘 배치 가능
  • HoverCard: 호버 미리보기. Avatar에 호버하면 프로필 카드 표시

조합 예제#

// 댓글 목록 패턴
ListView.builder(
  itemBuilder: (context, index) {
    final comment = comments[index];
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Avatar(
          imageUrl: comment.author.photoUrl,
          initials: Avatar.getInitials(comment.author.name),
          size: CoreComponentSize.sm,
          badge: comment.author.isOnline
              ? AvatarBadge(
                  avatarBadgeStyle: CoreAvatarBadgeStyle(
                    backgroundColor: CoreColor.token(CoreColors.success),
                  ),
                )
              : null,
        ),
        Gap(gapStyle: CoreGapStyle(size: CoreSpace.space12)),
        Expanded(child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(comment.author.name).bodyMedium.semiBold,
            Text(comment.body).bodySmall.onSurfaceVariant,
          ],
        )),
      ],
    );
  },
)