Quick take: Container is Flutter's general-purpose layout box. It combines width, height, padding, margin, alignment, and BoxDecoration in one widget. Use it for backgrounds, borders, rounded corners, gradients, and shadows. Reach for SizedBox if you only need sizing or Padding if you only need spacing. The most common pitfall: setting both color: and decoration: properties throws an assertion at runtime.
Container is the widget every Flutter tutorial reaches for first, and for good reason. It bundles a handful of single-purpose widgets (Padding, DecoratedBox, ConstrainedBox, Align) into one builder, so a UI box with padding, a coloured background, rounded corners, and a shadow takes one widget instead of four. That's convenience. The cost is that Container is the most often-overused widget in any Flutter codebase, including mine.
This tutorial covers the properties that come up day to day, the BoxDecoration sub-properties that handle visuals, the constraint behaviour that confuses everyone at first, and when to reach for a smaller widget instead.
What Does a Container Actually Do?
Container is a composition of smaller widgets. From the Flutter source, when you write:
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.blue),
width: 200,
height: 100,
child: const Text('Hello'),
)
The framework expands that to roughly:
Padding(
padding: const EdgeInsets.all(8),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 200, height: 100),
child: Padding(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
),
),
),
)
Same render result, more readable as Container. The Flutter docs even mention this composition explicitly. Knowing the expansion matters when debugging: if you see weird intermediate widgets in the inspector, it's because Container created them.
What Are the Sizing Properties?
The basics:
width: a double in logical pixels, ordouble.infinityto fill available widthheight: same, verticalconstraints: aBoxConstraintsfor finer control (minWidth,maxWidth,minHeight,maxHeight)
A few examples:
// Fixed size
Container(width: 100, height: 100, color: Colors.red)
// Fill the parent's available width, fixed height
Container(width: double.infinity, height: 50, color: Colors.blue)
// Constrained to a max width but flexible
Container(
constraints: const BoxConstraints(maxWidth: 400),
child: const Text('A long line of text that will wrap at 400px'),
)
The constraint gotcha: double.infinity only works if the parent provides bounded constraints in that axis. Inside a Column, the horizontal axis has bounded width (the column's width), so width: double.infinity is fine. The vertical axis is unbounded by default, so height: double.infinity throws. Inside a Scaffold's body, both axes are bounded, so either is fine.
If you've ever seen "RenderFlex children have non-zero flex but incoming height constraints are unbounded", that's the constraint rule biting. The fix is usually wrapping in Expanded (inside Row/Column) or setting a finite size on a parent.
How Does padding vs margin Work?
Same as CSS:
padding: space inside the Container, between its visual edge and the childmargin: space outside the Container, between its visual edge and siblings
Container(
margin: const EdgeInsets.all(20), // 20px gap outside
padding: const EdgeInsets.all(10), // 10px gap inside
color: Colors.green,
child: const Text('Hello'),
)
The green background extends to the padding edge but not into the margin. If you want background coverage all the way to the parent's edge, drop the margin and let the parent handle spacing.
EdgeInsets has several constructors:
EdgeInsets.all(16) // 16 on every side
EdgeInsets.symmetric(horizontal: 16, vertical: 8) // L+R = 16, T+B = 8
EdgeInsets.only(left: 16, top: 8) // Specific sides
EdgeInsets.fromLTRB(16, 8, 16, 8) // L T R B
I use symmetric more than the others, most real layouts have different horizontal and vertical padding.
How Does BoxDecoration Work?
BoxDecoration is where visuals happen, colour, gradient, border, border radius, shadow, background image. The full constructor:
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
width: 200,
height: 100,
)
That single widget gets you a blue rounded card with a black border and a soft drop shadow. Same effect in CSS would be background: blue; border-radius: 12px; border: 2px solid black; box-shadow: 0 4px 8px rgba(0,0,0,0.26), the mental model translates directly.
Gradients
For a gradient background, swap color for gradient:
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
width: 200,
height: 100,
)
LinearGradient covers most needs. RadialGradient and SweepGradient exist for circle and pie-slice patterns. The colors list can have any number of stops, with optional stops: to control distribution.
Border Radius Variants
Four common border radius patterns:
// Uniform on all corners
BorderRadius.circular(12)
// Different per corner
const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
)
// All round
BorderRadius.circular(999) // pill shape on a fixed-size box
// Horizontal vs vertical
const BorderRadius.vertical(top: Radius.circular(12))
BorderRadius.circular(999) on a square Container is the classic way to get a circle. Or use a CircleAvatar widget if you specifically want a circular image, that's purpose-built for the use case.
Shadows
The boxShadow property takes a list, so you can stack multiple shadows for depth:
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black12,
blurRadius: 16,
offset: const Offset(0, 8),
),
],
Two shadows, one tight and close, one diffuse and far. That's the Material Design elevation recipe in two BoxShadow entries.
The color vs decoration Rule
This catches every new Flutter dev. You can set color: directly on Container, or you can set decoration:. But not both:
// Throws an assertion at runtime
Container(
color: Colors.blue,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
)
The fix: move the colour into the decoration:
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
)
The Container constructor has an assert(color == null || decoration == null) that explicitly bans the combination, to prevent ambiguity about which one applies. Honestly, I think this is a poor API choice, every new Flutter dev trips on it. But the rule's been stable since 1.0 and won't change.
When Should You NOT Use a Container?
Three cases where a smaller widget reads better:
- Need only a fixed size? Use
SizedBox(width: 100, height: 50). No decoration, no padding, less indirection. - Need only padding? Use
Padding(padding: EdgeInsets.all(16), child: ...). Same. - Need only a coloured background? Use
ColoredBox(color: Colors.blue, child: ...). Faster than Container.
In a real codebase, you'll still reach for Container often, the moment you need two of those features it wins. But the inspector tree shows up cleaner if you use the specialised widget for single-concern cases.
How Do You Animate a Container?
AnimatedContainer is Container's drop-in animated cousin. Same properties, plus a duration and an optional curve:
class FadingBox extends StatefulWidget {
const FadingBox({super.key});
@override
State<FadingBox> createState() => _FadingBoxState();
}
class _FadingBoxState extends State<FadingBox> {
bool _toggled = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _toggled = !_toggled),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _toggled ? 200 : 100,
height: 100,
decoration: BoxDecoration(
color: _toggled ? Colors.purple : Colors.blue,
borderRadius: BorderRadius.circular(_toggled ? 24 : 8),
),
),
);
}
}
Tap to toggle, and the width, colour, and border radius all animate over 300ms. No AnimationController, no Tween, no listeners. For state-driven UI changes, AnimatedContainer is the lowest-effort option in the framework and covers maybe 70% of "I want this to animate" cases.
What Should You Build with Container?
Card layouts, list item rows, settings tiles, hero sections, gradient buttons, image overlays, custom badges. Anything that needs a styled box around content. Once Container clicks, you'll see it everywhere in real Flutter code, sometimes overused, sometimes correctly.
If you're starting out with Flutter, build a single card UI with a profile image (use a network ImageView equivalent, Image.network), a name, a description, and a few action buttons. Wrap each section in Container with appropriate padding and decoration. That's the building block for 80% of mobile UIs.
Related
- Flutter Tutorial Introduction - the broader framework, Dart language, hot reload, and renderer
- Android Introduction - the Android platform context if you're comparing native to Flutter
- Android Kotlin Introduction - the native Android alternative for direct comparison
- TypeScript Generics Explained - relevant if your other codebase is TypeScript and you're weighing the type systems