Fliin
On this page

Components / data-display

Avatar

Shows a person by their round photo, which fades in once loaded, or else by their initials or a silhouette.

16 of 16 variants pass
Avatar, photos state
Platform
Brightness
Text scale
Direction

Import

In a Flutter app where fliin init has run:

fliin add avatarAlso installs: core, icons

Examples

Photos

Photos from the app's assets. The initials stand in while a photo loads, or if it fails; semanticLabel names the person.

Avatar, photos state
import 'package:your_app/ui/fliin/components/avatar/fl_avatar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: [
        for (final (n, initials, name) in [
          (1, 'JS', 'Jane Smith'),
          (2, 'JD', 'John Doe'),
          (3, 'EC', 'Emily Chen'),
        ])
          FlAvatar(
            image: AssetImage('images/people/$n.jpg'),
            initials: initials,
            semanticLabel: name,
            size: FlAvatarSize.lg,
          ),
      ],
    );
  }
}

Content

Whatever the app has: a photo, initials, any icon, or nothing, in which case the avatar shows a person.

Avatar, content state
import 'package:your_app/ui/fliin/components/avatar/fl_avatar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: [
        const FlAvatar(
          image: AssetImage('images/people/4.jpg'),
          initials: 'MB',
          semanticLabel: 'Michael Brown',
          size: FlAvatarSize.lg,
        ),
        const FlAvatar(initials: 'KW', semanticLabel: 'Kate Wilson', size: FlAvatarSize.lg),
        FlAvatar(
          icon: FlIcon(FlIcons.shoppingCart),
          semanticLabel: 'Shop',
          size: FlAvatarSize.lg,
          variant: FlAvatarVariant.soft,
        ),
        const FlAvatar(semanticLabel: 'Guest', size: FlAvatarSize.lg),
      ],
    );
  }
}

Initials

Initials alone, in the accent color: for people without a photo.

Avatar, initials state
import 'package:your_app/ui/fliin/components/avatar/fl_avatar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: [
        for (final (initials, name) in [('JD', 'John Doe'), ('JS', 'Jane Smith'), ('BJ', 'Bob Johnson')])
          FlAvatar(initials: initials, semanticLabel: name, size: FlAvatarSize.lg, color: FlAvatarColor.accent),
      ],
    );
  }
}

Colors

Five colors, one row per variant: the color tells a status or a team apart, the variant sets how loud it is.

Avatar, colors state
import 'package:your_app/ui/fliin/components/avatar/fl_avatar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Column(
      mainAxisSize: MainAxisSize.min,
      spacing: space.md,
      children: [
        for (final variant in FlAvatarVariant.values)
          Wrap(
            alignment: WrapAlignment.center,
            spacing: space.sm,
            runSpacing: space.sm,
            children: [
              for (final (color, initials) in [
                (FlAvatarColor.neutral, 'DF'),
                (FlAvatarColor.accent, 'AC'),
                (FlAvatarColor.success, 'SC'),
                (FlAvatarColor.warning, 'WR'),
                (FlAvatarColor.danger, 'DG'),
              ])
                FlAvatar(initials: initials, semanticLabel: color.name, variant: variant, color: color),
            ],
          ),
      ],
    );
  }
}

Sizes

Every size, from the one beside a line of text to the one heading a profile.

Avatar, sizes state
import 'package:your_app/ui/fliin/components/avatar/fl_avatar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      crossAxisAlignment: WrapCrossAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: [
        for (final size in FlAvatarSize.values)
          FlAvatar(
            image: const AssetImage('images/people/5.jpg'),
            initials: 'LM',
            semanticLabel: 'Leo Martin',
            size: size,
          ),
      ],
    );
  }
}

Custom styles

Avatars adjusted one by one: a rounded square through radius, a team logo in its own colors through backgroundColor and foregroundColor, and a 24 byline through dimension.

Avatar, custom-styles state
import 'package:your_app/ui/fliin/components/avatar/fl_avatar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final colors = theme.colors;
    return Wrap(
      alignment: WrapAlignment.center,
      crossAxisAlignment: WrapCrossAlignment.center,
      spacing: theme.space.md,
      runSpacing: theme.space.md,
      children: [
        FlAvatar(
          image: const AssetImage('images/people/6.jpg'),
          initials: 'JD',
          semanticLabel: 'John Doe',
          size: FlAvatarSize.lg,
          radius: theme.shape.radiusXl,
        ),
        FlAvatar(
          initials: 'GB',
          semanticLabel: 'Green Bay team',
          size: FlAvatarSize.lg,
          radius: theme.shape.radiusXl,
          backgroundColor: colors.success,
          foregroundColor: colors.onSuccess,
        ),
        const FlAvatar(
          image: AssetImage('images/people/3.jpg'),
          initials: 'EC',
          semanticLabel: 'Emily Chen',
          size: FlAvatarSize.sm,
          dimension: 24,
        ),
      ],
    );
  }
}

Usage

The code the reference images render:

final example = ColoredBox(
  color: FlScope.of(context).colors.surface,
  child: Padding(
    padding: EdgeInsetsDirectional.all(FlScope.of(context).space.lg),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      spacing: FlScope.of(context).space.md,
      children: [
        for (final v in FlAvatarVariant.values)
          Wrap(
            spacing: FlScope.of(context).space.sm,
            runSpacing: FlScope.of(context).space.sm,
            children: [
              for (final c in FlAvatarColor.values)
                FlAvatar(
                  initials: 'JD',
                  semanticLabel: 'Jane Doe',
                  variant: v,
                  color: c,
                ),
            ],
          ),
        Wrap(
          spacing: FlScope.of(context).space.sm,
          crossAxisAlignment: WrapCrossAlignment.center,
          children: [
            for (final s in FlAvatarSize.values)
              FlAvatar(size: s, semanticLabel: 'Guest'),
          ],
        ),
      ],
    ),
  ),
);

API

FlAvatarSize: enum {sm, md, lg, xl}
FlAvatarSize.sm: static final FlAvatarSize
FlAvatarSize.md: static final FlAvatarSize
FlAvatarSize.lg: static final FlAvatarSize
FlAvatarSize.xl: static final FlAvatarSize
FlAvatarVariant: enum {primary, soft}
FlAvatarVariant.primary: static final FlAvatarVariant
FlAvatarVariant.soft: static final FlAvatarVariant
FlAvatarColor: enum {accent, neutral, success, warning, danger}
FlAvatarColor.accent: static final FlAvatarColor
FlAvatarColor.neutral: static final FlAvatarColor
FlAvatarColor.success: static final FlAvatarColor
FlAvatarColor.warning: static final FlAvatarColor
FlAvatarColor.danger: static final FlAvatarColor
FlAvatar: class extends StatelessWidget
FlAvatar.new: const new({super.key, ImageProvider? image, String? initials, Widget? icon, String? semanticLabel, FlAvatarSize size, FlAvatarVariant variant, FlAvatarColor color, Duration fallbackDelay, double? dimension, double? radius, Color? backgroundColor, Color? foregroundColor})
FlAvatar.image: get ImageProvider?
FlAvatar.initials: get String?
FlAvatar.icon: get Widget?
FlAvatar.semanticLabel: get String?
FlAvatar.size: get FlAvatarSize
FlAvatar.variant: get FlAvatarVariant
FlAvatar.color: get FlAvatarColor
FlAvatar.fallbackDelay: get Duration
FlAvatar.dimension: get double?
FlAvatar.radius: get double?
FlAvatar.backgroundColor: get Color?
FlAvatar.foregroundColor: get Color?
FlAvatar.build: Widget(BuildContext context)

Reference renders

The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify avatar compares your copy to them.

iOS · Light
Avatar, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Avatar, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Avatar, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Avatar, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Avatar, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Avatar, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Avatar, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Avatar, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Avatar, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Avatar, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Avatar, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Avatar, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Avatar, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Avatar, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Avatar, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Avatar, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • show a user's profile picture
  • show initials when the photo is missing
  • present the author of a message
  • show the signed-in account at the top of the screen
  • show who takes part in a conversation
  • show a user photo in a circle
Not for
  • a status or category in text: use a chip (chip)

Source

The files fliin add avatar copies into your app, before your prefix and imports are applied.

avatar/fl_avatar.dart

// Modified from the Avatar of HeroUI Native 1.0.10, Copyright 2025 NextUI Inc.,
// licensed under Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0
import 'package:flutter/widgets.dart';
import 'package:fliin_ui/core/fliin.dart';
import 'package:fliin_ui/icons/fl_icons.dart';

/// The side of a [FlAvatar], and the size of its text and icon.
enum FlAvatarSize {
  /// 40, `textXs` text, 14 icon.
  sm,

  /// 48, `textSm` text, 16 icon.
  md,

  /// 64, `textBase` text, 20 icon.
  lg,

  /// 88, `heading2` text, 32 icon: the head of a profile (Izydesk's
  /// `.avatar-xl`).
  xl,
}

/// The background of a [FlAvatar].
enum FlAvatarVariant {
  /// The `control` background, whatever the color.
  primary,

  /// The color's tint at 15 %, `control` for the neutral.
  soft,
}

/// The color of a [FlAvatar]: that of the initials and of the fallback
/// icon, and of the `soft` background.
enum FlAvatarColor {
  /// The accent.
  accent,

  /// The neutral.
  neutral,

  /// Green.
  success,

  /// Orange.
  warning,

  /// Red.
  danger,
}

/// An avatar: a round image that fades in once loaded,
/// and, without an image or when it fails, a fallback (initials, a given
/// icon, or a silhouette) that fades in. While loading, only the background
/// shows.
class FlAvatar extends StatelessWidget {
  /// An `md`, `primary`, `accent` avatar.
  const FlAvatar({
    super.key,
    this.image,
    this.initials,
    this.icon,
    this.semanticLabel,
    this.size = FlAvatarSize.md,
    this.variant = FlAvatarVariant.primary,
    this.color = FlAvatarColor.accent,
    this.fallbackDelay = Duration.zero,
    this.dimension,
    this.radius,
    this.backgroundColor,
    this.foregroundColor,
  }) : assert(dimension == null || dimension > 0, 'dimension must be positive'),
       assert(radius == null || radius >= 0, 'radius must not be negative');

  /// The image; when null, the fallback.
  final ImageProvider? image;

  /// The fallback text, most often two letters, in medium weight. It does
  /// not grow beyond 1.4 times its size.
  final String? initials;

  /// The fallback without [initials], in the icon's color and size; when
  /// null, the `person` icon of the chosen set.
  final Widget? icon;

  /// What the screen reader reads: the person's name. When null: the
  /// [initials].
  final String? semanticLabel;

  /// The side.
  final FlAvatarSize size;

  /// The background.
  final FlAvatarVariant variant;

  /// The color.
  final FlAvatarColor color;

  /// The wait before the fallback enters: an image
  /// that fails quickly does not make the initials flicker.
  final Duration fallbackDelay;

  /// The side, instead of that of [size], which keeps its text and icon: a
  /// 24 byline under a card, a 56 community logo.
  final double? dimension;

  /// The radius of the continuous corners; when null,
  /// `shape.radiusButtonLarge`, a disc up to the `lg` size. `shape.radiusXl`
  /// draws a rounded square.
  final double? radius;

  /// The background, instead of that of [variant] and [color].
  final Color? backgroundColor;

  /// The color of the initials and of the fallback icon, instead of that of
  /// [color].
  final Color? foregroundColor;

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final colors = theme.colors;
    final motion = FlMotion.of(context);
    final soft = variant == FlAvatarVariant.soft;
    final fill =
        backgroundColor ??
        switch (color) {
          FlAvatarColor.accent when soft => colors.accentSoft,
          FlAvatarColor.success when soft => colors.successSoft,
          FlAvatarColor.warning when soft => colors.warningSoft,
          FlAvatarColor.danger when soft => colors.dangerSoft,
          _ => colors.control,
        };
    final ink =
        foregroundColor ??
        switch (color) {
          FlAvatarColor.accent => colors.onAccentSoft,
          FlAvatarColor.neutral => colors.onControl,
          FlAvatarColor.success => colors.onSuccessSoft,
          FlAvatarColor.warning => colors.onWarningSoft,
          // On `control`, `danger` holds 4.5:1; `onDangerSoft` is darker only
          // for the soft tint.
          FlAvatarColor.danger => soft ? colors.onDangerSoft : colors.danger,
        };
    final xs = theme.space.xs;
    // size-10, size-12, size-16; the icons from AVATAR_DEFAULT_ICON_SIZE.
    final (sizeSide, role, iconSize) = switch (size) {
      FlAvatarSize.sm => (xs * 10, FlTextRole.textXs, 14.0),
      FlAvatarSize.md => (xs * 12, FlTextRole.textSm, 16.0),
      FlAvatarSize.lg => (xs * 16, FlTextRole.textBase, 20.0),
      FlAvatarSize.xl => (xs * 22, FlTextRole.heading2, 32.0),
    };
    final side = dimension ?? sizeSide;
    final fade = motion.reduced ? Duration.zero : theme.motion.short4;
    final curve = theme.motion.easeIn;
    Widget fallback() {
      final initials = this.initials;
      final scale = theme.typography;
      final content = initials != null
          ? MediaQuery.withClampedTextScaling(
              maxScaleFactor: _maxTextScale,
              child: Text(
                initials,
                maxLines: 1,
                textAlign: TextAlign.center,
                style: scale.resolve(role).copyWith(color: ink, fontWeight: theme.tokens.typography.weight.medium),
                strutStyle: scale.strut(role, inheritedFamily: DefaultTextStyle.of(context).style.fontFamily),
              ),
            )
          : IconTheme.merge(
              data: IconThemeData(color: ink, size: iconSize),
              child: icon ?? const _PersonIcon(),
            );
      return _FadeIn(
        delay: motion.reduced ? Duration.zero : fallbackDelay,
        duration: fade,
        curve: curve,
        child: Center(child: content),
      );
    }

    final image = this.image;
    final shape = FlContinuousBorder(radius: radius ?? theme.shape.radiusButtonLarge);
    return Semantics(
      image: true,
      label: semanticLabel ?? initials,
      child: ExcludeSemantics(
        child: FlPixelSnap(
          child: SizedBox.square(
            dimension: side,
            // The background is painted to the shape, without clipping:
            // clipped as well, its doubly antialiased edge fades (0.63 px at
            // the outline in sm). Only the image, which overflows, is clipped.
            child: DecoratedBox(
              decoration: ShapeDecoration(color: fill, shape: shape),
              child: image == null
                  ? fallback()
                  : ClipPath(
                      clipper: ShapeBorderClipper(shape: shape, textDirection: Directionality.maybeOf(context)),
                      child: Image(
                        image: image,
                        fit: BoxFit.cover,
                        width: side,
                        height: side,
                        frameBuilder: (context, child, frame, synchronous) => frame == null
                            ? const SizedBox.expand()
                            : _FadeIn(duration: fade, curve: curve, child: child),
                        errorBuilder: (context, error, stack) => fallback(),
                      ),
                    ),
            ),
          ),
        ),
      ),
    );
  }
}

/// The largest text scale the initials follow.
const double _maxTextScale = 1.4;

/// A fade-in of [duration] along [curve], after [delay]: Reanimated's
/// `FadeIn`, replayed on every mount.
class _FadeIn extends StatefulWidget {
  const _FadeIn({this.delay = Duration.zero, required this.duration, required this.curve, required this.child});

  final Duration delay;
  final Duration duration;
  final Curve curve;
  final Widget child;

  @override
  State<_FadeIn> createState() => _FadeInState();
}

class _FadeInState extends State<_FadeIn> with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    vsync: this,
    duration: widget.delay + widget.duration,
    value: widget.delay + widget.duration == Duration.zero ? 1 : 0,
  )..forward();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final total = (widget.delay + widget.duration).inMicroseconds;
    final start = total == 0 ? 0.0 : widget.delay.inMicroseconds / total;
    return FadeTransition(
      opacity: CurvedAnimation(
        parent: _controller,
        curve: Interval(start, 1, curve: widget.curve),
      ),
      child: widget.child,
    );
  }
}

/// The `person` icon of the chosen set, in the color and size of the
/// `IconTheme`.
class _PersonIcon extends StatelessWidget {
  const _PersonIcon();

  @override
  Widget build(BuildContext context) {
    final icon = IconTheme.of(context);
    return FlIcon(FlIcons.person, size: icon.size ?? 16, color: icon.color ?? FlScope.of(context).colors.onControl);
  }
}

Arrows to move, Enter to open, Escape to close.