All widgets
Widget

Custom Avatar with Initials

A production-ready avatar widget that shows a profile image when available and gracefully falls back to styled initials — just like Google, Slack, and Microsoft Teams. Same name always gets the same color.

FlutterDartFlutterFlow
View on GitHub

How it looks

B
Brani
JD
Jane Doe
MK
Max Klein
AL
Anna L.
Image loaded
?
Broken URL

Same name = same color. Always.

What's included

Features

  • 🖼️Image with fallback — shows profile photo or auto-generated initials
  • 🎨Deterministic colors — same name always gets the same background color
  • 🛡️Error resilient — broken image URLs automatically fall back to initials
  • Circular or rounded — supports both shapes via parameters
  • Optional fade-in — smooth image animation on load
  • 🎯Fully configurable — size, colors, border, font weight, radius
  • 🔗FlutterFlow friendly — separate firstName + lastName params match typical data models
  • 🚀Zero dependencies — pure Flutter
ℹ️

Why this widget exists

FlutterFlow's CircleImage widget crashes on empty or broken image URLs. This widget solves that — it always renders something meaningful. Pass a URL and it shows the image. Don't pass one (or it breaks) — it shows initials. No crashes, no blank circles, no extra logic needed.

Integration

How to use it

1

Create a Custom Widget

In FlutterFlow, go to Custom Code → Custom Widgets → + Create. Name it exactly `CustomAvatar`. Set the default width and height to `48`.

2

Add the parameters

Add all 13 parameters from the table below. Only `firstName` is required — all others are optional with sensible defaults.

3

Paste the code & compile

Paste the full widget code from the section below. Hit Save — FlutterFlow will compile it automatically.

4

Use it anywhere

Drop CustomAvatar onto any page. Pass `firstName` and optionally `lastName` and `imageUrl`. The widget handles the rest automatically.

API reference

Parameters

ParameterTypeDefaultDescription
firstNameStringFirst name (required) — used for initials and color
lastNameStringLast name (optional) — adds second initial
imageUrlStringProfile image URL — falls back to initials if empty or broken
sizedouble48Avatar diameter in pixels
isCircularbooltrueCircular shape (true) or rounded rectangle (false)
enableAnimationboolfalseFade-in animation when the image loads
borderRadiusdouble8.0Corner radius (only applies when isCircular is false)
borderColorColortransparentOptional border color
borderWidthdouble0Border thickness
fontSizedoubleautoInitials font size (auto-scales with avatar size by default)
fontWeightint600Initials font weight (400–900)
textColorColorWhiteInitials text color
backgroundColorColorautoOverride the auto-assigned background color

Ready to copy

Preset examples

Basic — First name only

dart
CustomAvatar(
  firstName: "Brani",
)
// → Shows "B" with deterministic color

With last name

dart
CustomAvatar(
  firstName: "Brani",
  lastName: "Mueller",
)
// → Shows "BM" with deterministic color

With profile image

dart
CustomAvatar(
  firstName: "Jane",
  lastName: "Doe",
  imageUrl: "https://example.com/avatar.jpg",
)
// → Shows image, falls back to "JD" if broken

Large rounded with border

dart
CustomAvatar(
  firstName: "Jane",
  lastName: "Doe",
  size: 80,
  isCircular: false,
  borderRadius: 16,
  borderColor: Color(0xFFE0E0E0),
  borderWidth: 2,
  enableAnimation: true,
)

Full source

Full widget code

Copy the entire file into FlutterFlow → Custom Code → Custom Widgets.

dart
// Automatic FlutterFlow imports
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

class CustomAvatar extends StatefulWidget {
  const CustomAvatar({
    super.key,
    this.width,
    this.height,
    required this.firstName,
    this.lastName,
    this.imageUrl,
    this.size,
    this.isCircular,
    this.enableAnimation,
    this.borderRadius,
    this.borderColor,
    this.borderWidth,
    this.fontSize,
    this.fontWeight,
    this.textColor,
    this.backgroundColor,
  });

  final double? width;
  final double? height;
  final String firstName;
  final String? lastName;
  final String? imageUrl;
  final double? size;
  final bool? isCircular;
  final bool? enableAnimation;
  final double? borderRadius;
  final Color? borderColor;
  final double? borderWidth;
  final double? fontSize;
  final int? fontWeight;
  final Color? textColor;
  final Color? backgroundColor;

  @override
  State<CustomAvatar> createState() => _CustomAvatarState();
}

class _CustomAvatarState extends State<CustomAvatar> {
  // ── Color Palette ────────────────────────────────────────
  static const List<Color> _palette = [
    Color(0xFF4A90D9), // Blue
    Color(0xFF50B86C), // Green
    Color(0xFFE85D75), // Rose
    Color(0xFFF5A623), // Amber
    Color(0xFF9B59B6), // Purple
    Color(0xFF1ABC9C), // Teal
    Color(0xFFE67E22), // Orange
    Color(0xFF3498DB), // Sky Blue
    Color(0xFFE74C3C), // Red
    Color(0xFF2ECC71), // Emerald
    Color(0xFF8E44AD), // Deep Purple
    Color(0xFFF39C12), // Sunflower
    Color(0xFF16A085), // Dark Teal
    Color(0xFFD35400), // Pumpkin
    Color(0xFF2980B9), // Strong Blue
    Color(0xFFC0392B), // Dark Red
  ];

  String _getInitials() {
    final first = widget.firstName.trim();
    final last = (widget.lastName ?? '').trim();

    if (first.isEmpty && last.isEmpty) return '?';
    if (first.isEmpty) return last[0].toUpperCase();
    if (last.isEmpty) return first[0].toUpperCase();

    return '${first[0]}${last[0]}'.toUpperCase();
  }

  Color _getColorForName() {
    final combined =
        '${widget.firstName.trim()} ${(widget.lastName ?? '').trim()}'.trim();
    if (combined.isEmpty) return _palette[0];
    int hash = 0;
    for (int i = 0; i < combined.length; i++) {
      hash = combined.codeUnitAt(i) + ((hash << 5) - hash);
    }
    return _palette[hash.abs() % _palette.length];
  }

  bool get _hasImage =>
      widget.imageUrl != null && widget.imageUrl!.trim().isNotEmpty;

  @override
  Widget build(BuildContext context) {
    final double avatarSize = widget.size ?? 48;
    final bool circular = widget.isCircular ?? true;
    final double radius =
        circular ? avatarSize / 2 : (widget.borderRadius ?? 12);
    final double bWidth = widget.borderWidth ?? 0;
    final Color bColor = widget.borderColor ?? Colors.transparent;
    final Color bgColor = widget.backgroundColor ?? _getColorForName();
    final Color fgColor = widget.textColor ?? Colors.white;
    final double fSize = widget.fontSize ?? (avatarSize * 0.38);
    final FontWeight fWeight =
        FontWeight.values[(widget.fontWeight ?? 6).clamp(0, 8)];
    final String initials = _getInitials();
    final bool animate = widget.enableAnimation ?? false;

    return Container(
      width: avatarSize,
      height: avatarSize,
      decoration: BoxDecoration(
        color: bgColor,
        borderRadius: BorderRadius.circular(radius),
        border: bWidth > 0 ? Border.all(color: bColor, width: bWidth) : null,
      ),
      clipBehavior: Clip.antiAlias,
      child: _hasImage
          ? Image.network(
              widget.imageUrl!,
              width: avatarSize,
              height: avatarSize,
              fit: BoxFit.cover,
              frameBuilder: animate
                  ? (context, child, frame, wasSynchronouslyLoaded) {
                      if (wasSynchronouslyLoaded) return child;
                      return AnimatedOpacity(
                        opacity: frame == null ? 0 : 1,
                        duration: const Duration(milliseconds: 300),
                        curve: Curves.easeOut,
                        child: child,
                      );
                    }
                  : null,
              errorBuilder: (context, error, stackTrace) {
                return Center(
                  child: Text(
                    initials,
                    style: TextStyle(
                      color: fgColor,
                      fontSize: fSize,
                      fontWeight: fWeight,
                      letterSpacing: 0.5,
                      height: 1,
                    ),
                    textAlign: TextAlign.center,
                  ),
                );
              },
            )
          : Center(
              child: Text(
                initials,
                style: TextStyle(
                  color: fgColor,
                  fontSize: fSize,
                  fontWeight: fWeight,
                  letterSpacing: 0.5,
                  height: 1,
                ),
                textAlign: TextAlign.center,
              ),
            ),
    );
  }
}

MIT — Free to use in personal and commercial projects.

From the trenches

Pro tips

Deterministic colors:: The widget picks from 16 curated colors based on the user's name — same name always gets the same color. No randomness, no chaos in lists.

Always pass firstName:: It's the only required parameter and drives both the initials and the color. Even a single letter works perfectly.

imageUrl is safe to leave empty:: If the URL is empty or fails to load, the widget silently falls back to initials — no broken image icons.

For chat apps:: Use size 36–40 for message list avatars, size 56–64 for profile headers. The widget scales cleanly at any size.

Building an app?

Need a full MVP, not just widgets?

I build complete apps for founders — fixed prices, fast delivery. Book a free 30-minute call and let's talk about your idea.

Book a free call