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.
How it looks
Same name = same color. Always.
What's included
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
In FlutterFlow, go to Custom Code → Custom Widgets → + Create. Name it exactly `CustomAvatar`. Set the default width and height to `48`.
Add all 13 parameters from the table below. Only `firstName` is required — all others are optional with sensible defaults.
Paste the full widget code from the section below. Hit Save — FlutterFlow will compile it automatically.
Drop CustomAvatar onto any page. Pass `firstName` and optionally `lastName` and `imageUrl`. The widget handles the rest automatically.
API reference
| Parameter | Type | Default | Description |
|---|---|---|---|
| firstName | String | — | First name (required) — used for initials and color |
| lastName | String | — | Last name (optional) — adds second initial |
| imageUrl | String | — | Profile image URL — falls back to initials if empty or broken |
| size | double | 48 | Avatar diameter in pixels |
| isCircular | bool | true | Circular shape (true) or rounded rectangle (false) |
| enableAnimation | bool | false | Fade-in animation when the image loads |
| borderRadius | double | 8.0 | Corner radius (only applies when isCircular is false) |
| borderColor | Color | transparent | Optional border color |
| borderWidth | double | 0 | Border thickness |
| fontSize | double | auto | Initials font size (auto-scales with avatar size by default) |
| fontWeight | int | 600 | Initials font weight (400–900) |
| textColor | Color | White | Initials text color |
| backgroundColor | Color | auto | Override the auto-assigned background color |
Ready to copy
CustomAvatar(
firstName: "Brani",
)
// → Shows "B" with deterministic colorCustomAvatar(
firstName: "Brani",
lastName: "Mueller",
)
// → Shows "BM" with deterministic colorCustomAvatar(
firstName: "Jane",
lastName: "Doe",
imageUrl: "https://example.com/avatar.jpg",
)
// → Shows image, falls back to "JD" if brokenCustomAvatar(
firstName: "Jane",
lastName: "Doe",
size: 80,
isCircular: false,
borderRadius: 16,
borderColor: Color(0xFFE0E0E0),
borderWidth: 2,
enableAnimation: true,
)Full source
Copy the entire file into FlutterFlow → Custom Code → Custom Widgets.
// 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
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.
Free & open source
Drop-in shimmer loading animation. Five layout presets, fully customizable.
Smart text truncation with '...' append. Zero dependencies, pure Dart.
Customizable dotted and dashed borders. Because FlutterFlow doesn't have this built in.
3D flip animation between front and back content. Tap to reveal.
Building an app?
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