“Half the custom widgets I’ve written over the years were just me rebuilding something that already shipped in the box.”

There’s a pattern I’ve noticed in my own code that’s a little embarrassing to admit. Every few months, I hit a UI problem, reach straight for a custom solution, spend a chunk of time building it — and then find out, sometimes days later, that Flutter already had a widget for exactly that.
It’s not that these widgets are obscure. They’re sitting right there in the SDK, documented, stable, boring in the best way. I just don’t reach for them by default, because Container, Row, Column, and GestureDetector become muscle memory early, and everything past that first layer tends to stay unexplored unless something forces you to go looking.
This is less a list and more a running account of the ones that actually changed how I build once I stopped skipping past them.
The Overflow Problem We All Just Accept 🏷️
“That yellow-and-black stripe pattern might be the most universally recognized error in all of Flutter.”
Tag lists, filter chips, category pickers — anything with a variable number of small items tends to start life inside a Row, because that's the obvious choice. It works fine right up until one more chip gets added than the screen has room for, and suddenly you're staring at an overflow warning in production.
Wrap solves this so completely that it almost feels unfair how rarely people reach for it first:
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('Firebase')),
Chip(label: Text('Riverpod')),
],
)
Items that don’t fit just move to the next line. No manual width math, no horizontal-scroll workaround standing in for a layout bug. I’ve built that workaround more than once in the past, back before I actually understood this was the fix the whole time.
Copy-Paste Shouldn’t Require Custom Work 📋
“An order number nobody can copy isn’t a small bug. It’s a support ticket waiting to happen.”
Text looks selectable. It isn't. Anywhere you're showing something a user might genuinely want to copy — an ID, a reference number, an error code, a log line — plain Text quietly fails them the moment they try to long-press it.
SelectableText(
'ORD-2026-88421',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
)
Same API, same styling, and the only real difference is that it actually works the way users assume text already works. This is one of those fixes that costs nothing and only shows up as a problem once someone’s frustrated enough to complain about it.
Responsive Isn’t What Most of Us Think It Is 📐
“Checking the device’s screen width when you only own a fraction of that screen is checking the wrong number entirely.”
A lot of “responsive” Flutter code boils down to a MediaQuery.of(context).size.width check somewhere near the top of a build method. That's a reasonable instinct for a full-screen layout — and the wrong one the moment your widget only owns part of the screen, inside a split view, a dialog, or a resizable panel.
LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 600;
return isWide ? const DesktopLayout() : const MobileLayout();
},
)
LayoutBuilder hands you the actual space your widget has to work with, not the device's. It's a small distinction that only matters once you're building something that genuinely needs to adapt to its container instead of the screen — and once you need it, nothing else quite substitutes.
The Gesture I Kept Rebuilding From Scratch 👈
“Swipe-to-delete feels like it should be simple. Building the drag physics yourself is where that feeling stops being true.”
Task lists, inbox-style UIs, anything with a “swipe to remove” interaction — it’s tempting to reach for a raw GestureDetector, track the drag offset manually, and build your own reveal-and-delete animation on top of it. I've done exactly that more than once, fully convinced it was necessary.
Dismissible(
key: ValueKey(task.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete),
),
onDismissed: (_) => tasks.remove(task),
child: ListTile(title: Text(task.title)),
)
The drag threshold, the reveal animation, the dismiss physics — all of it’s already handled. Every hour I’ve spent building a worse version of this by hand is an hour I didn’t need to spend.
Animations Don’t Always Need a Controller 🔄
“AnimationController, TickerProviderStateMixin, a dispose method to remember — for a fade. There’s usually a smaller tool for this job.”
A hard cut between a loading spinner and real content reads as cheap, even in an otherwise polished app. The instinct is to reach for a full AnimationController setup — which is the right tool for genuinely complex animation, and a lot of ceremony for something this simple.
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLoading
? const CircularProgressIndicator(key: ValueKey('loading'))
: const Text('Content loaded', key: ValueKey('content')),
)
Give each state its own ValueKey so Flutter treats them as genuinely different widgets worth transitioning between, and that's the whole implementation. No controller to manage, no dispose method to remember to write, no ticker mixin.
The State Management I Didn’t Actually Need 🔔
“Not every toggle needs a provider wired up behind it.”
There’s a specific trap I’ve watched myself fall into more than once: a single boolean, a single counter, some genuinely local piece of state that only one widget cares about — and somehow it still ends up routed through whatever full state-management setup the rest of the app uses, just out of habit.
final isExpanded = ValueNotifier<bool>(false);
ValueListenableBuilder<bool>(
valueListenable: isExpanded,
builder: (context, expanded, child) {
return Icon(expanded ? Icons.expand_less : Icons.expand_more);
},
)
Flip the value with isExpanded.value = !isExpanded.value, and only the widget actually listening rebuilds. No provider, no global store, no boilerplate for state that was never going to leave this one widget's scope anyway. It's a good reminder that reaching for the app's default state solution isn't always the same as reaching for the right one.
The Icon Button Nobody Explains 💬
“An icon-only button is a small mystery to anyone who hasn’t already memorized your icon set — which, on a first visit, is everyone.”
Admin dashboards and desktop apps in particular tend to accumulate rows of icon-only buttons — delete, refresh, export, archive — all assuming the user already knows what each glyph means. On mobile that’s a stretch. On desktop, where hovering is free and nobody’s using it, it’s a missed opportunity.
Tooltip(
message: 'Delete item',
child: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {},
),
)
One wrap, and every icon-only control in the interface explains itself the moment someone hovers or long-presses. It’s such a small addition that it’s easy to treat as optional polish — but for anyone unfamiliar with the interface, it’s closer to a usability requirement than a nice-to-have.
The Math I Used to Do by Hand 🎬
“I used to calculate width * 9 / 16 manually and store it in a variable called something like calculatedHeight. There was never a good reason for that."
Video thumbnails, camera previews, image cards — anything that needs to hold a consistent shape regardless of how much width it’s given used to mean scattering manual ratio math through my build methods, recalculating it slightly differently in every file that needed it.
AspectRatio(
aspectRatio: 16 / 9,
child: VideoThumbnail(url: thumbnailUrl),
)
It works out the correct height from whatever width it’s handed, every single time, without a division operator anywhere in sight. Small thing, but it’s the kind of small thing that used to eat more mental energy than it had any right to.
The One Worth Using Carefully, Not Constantly ⚠️
“Not every widget on this list is a free win. This one’s genuinely useful and genuinely easy to overuse.”
IntrinsicHeight and IntrinsicWidth size themselves based on their children's natural dimensions — useful when you need siblings inside a Row to match each other's height without hardcoding a number:
IntrinsicHeight(
child: Row(
children: [
Container(width: 4, color: Colors.blue),
const SizedBox(width: 12),
const Expanded(child: Text('This content determines the height.')),
],
),
)
Worth knowing upfront: this comes with a real performance cost, because Flutter has to run extra layout passes to figure out those intrinsic dimensions. It’s fine used sparingly. Drop one of these inside a large scrolling list and you’ll feel the difference in frame times pretty quickly. This is one I reach for deliberately now, not reflexively — which is a different lesson than most of the others here, and worth keeping in mind before you go widget-hunting through the rest of the SDK.
How I Actually Go Looking for These Now 🔍
I stopped treating “check if Flutter already has this” as an occasional afterthought and started treating it as the actual first step, before opening a blank file to build something custom. In practice that means a genuinely boring habit: search the widget catalog or API docs for the behavior I want, not the widget name I already know, since the whole problem is not knowing the name yet. IDE autocomplete inside a Widget return type does more of this work than people give it credit for too — typing the first few letters of what you're picturing something being called surfaces a real answer more often than expected.
None of that is a clever trick. It’s closer to a discipline than a technique, and it’s the actual reason this list keeps getting longer for me over time instead of staying fixed.
None of these are individually hard problems. That’s what actually stuck with me once I noticed the pattern — every one of them had a boringly simple, already-shipped fix, and the only reason I didn’t reach for it sooner was that writing something custom felt like the default move rather than a choice I was consciously making.
The habit that’s genuinely saved me time since isn’t memorizing a longer list of widgets. It’s pausing for one extra beat before writing anything custom and asking a boring question first: does Flutter already ship something for this? More often than feels reasonable, it does.
What’s yours — the widget you built a worse version of by hand before finding out it already existed? Genuinely curious what I’m still missing. Save this one for the next time you’re mid-overflow-warning, and drop your own story below.
Tags: Flutter, Flutter Widgets, Mobile Development, Dart, Flutter UI, Software Engineering
