ConstrainedBox, FittedBox & AspectRatio
Understanding Layout Constraints
Flutter’s layout system is built on the concept of constraints. Every widget receives constraints from its parent (minimum and maximum width/height) and must choose a size within those bounds. In this lesson, we explore the widgets that let you explicitly control, override, and adapt constraints to build precise, responsive layouts.
ConstrainedBox
ConstrainedBox imposes additional constraints on its child. It uses a BoxConstraints object that defines minWidth, maxWidth, minHeight, and maxHeight. The final constraints applied to the child are the intersection of the parent’s constraints and the ones you specify.
Basic ConstrainedBox Usage
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 100,
maxWidth: 300,
minHeight: 50,
maxHeight: 200,
),
child: Container(
color: Colors.blue,
child: const Text('I am constrained!'),
),
)
Common BoxConstraints factory constructors simplify common patterns:
BoxConstraints Factories
// Force an exact size
BoxConstraints.tight(const Size(200, 100))
// Allow any size up to the given dimensions
BoxConstraints.loose(const Size(300, 200))
// Expand to fill all available space
const BoxConstraints.expand()
// Fixed width, flexible height
const BoxConstraints.tightFor(width: 250)
// Constrain only one axis
const BoxConstraints(
maxWidth: 400,
// minWidth defaults to 0.0
// height is unconstrained
)
ConstrainedBox can only tighten constraints—it cannot make them looser. If the parent already says “must be exactly 100 wide,” a ConstrainedBox with maxWidth: 300 has no effect.
UnconstrainedBox
UnconstrainedBox removes the parent’s constraints entirely, allowing its child to render at its natural (intrinsic) size. This is useful when a parent forces a widget to expand but you want the child to be smaller. Be careful—if the child overflows, Flutter will show an overflow warning.
UnconstrainedBox Example
// Inside a Row that stretches children
Row(
children: [
Expanded(
child: UnconstrainedBox(
child: Container(
width: 80,
height: 40,
color: Colors.green,
child: const Center(
child: Text('Free!'),
),
),
),
),
],
)
LimitedBox
LimitedBox limits its child’s size only when the incoming constraints are unbounded. This is especially useful inside scrollable views like ListView where height is unconstrained.
LimitedBox in a ListView
ListView(
children: [
LimitedBox(
maxHeight: 200,
child: Container(
color: Colors.orange,
child: const Center(
child: Text('Max 200px tall in a list'),
),
),
),
],
)
OverflowBox
OverflowBox passes different constraints to its child than it received from its own parent. Unlike ConstrainedBox, it can actually loosen constraints. The child may paint outside the OverflowBox’s bounds.
OverflowBox Example
SizedBox(
width: 100,
height: 100,
child: OverflowBox(
maxWidth: 200,
maxHeight: 200,
child: Container(
width: 200,
height: 200,
color: Colors.red.withOpacity(0.5),
child: const Center(
child: Text('I overflow!'),
),
),
),
)
FittedBox
FittedBox scales and positions its child within itself according to a BoxFit mode. It is particularly useful for scaling text, images, or entire widget trees to fit a given space.
FittedBox with BoxFit Modes
// Scale text to fill the width
SizedBox(
width: 300,
height: 50,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'This text shrinks if too wide',
style: const TextStyle(fontSize: 40),
),
),
)
// Common BoxFit values:
// BoxFit.contain - Scale to fit, maintain aspect ratio
// BoxFit.cover - Scale to fill, maintain aspect ratio, may clip
// BoxFit.fill - Stretch to fill, may distort
// BoxFit.fitWidth - Scale width to fit, may overflow height
// BoxFit.fitHeight - Scale height to fit, may overflow width
// BoxFit.scaleDown - Like contain but only scales down
// BoxFit.none - No scaling, center the child
FittedBox requires its child to have a finite size. If the child tries to be infinitely wide (e.g., an unconstrained Row), Flutter will throw an error. Always ensure the child has bounded dimensions.
Practical Example: Responsive Image
FittedBox for Responsive Images
class ResponsiveImage extends StatelessWidget {
final String imageUrl;
const ResponsiveImage({super.key, required this.imageUrl});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 600,
maxHeight: 400,
),
child: FittedBox(
fit: BoxFit.contain,
child: Image.network(imageUrl),
),
);
}
}
AspectRatio
The AspectRatio widget sizes its child to a specific aspect ratio. It first tries to match the largest width permitted by the constraints, then sets the height according to the ratio. If the parent provides tight constraints, the aspect ratio may not be achievable.
AspectRatio Widget
// 16:9 video player container
AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Colors.black,
child: const Center(
child: Icon(
Icons.play_circle_outline,
color: Colors.white,
size: 64,
),
),
),
)
// Square container
AspectRatio(
aspectRatio: 1.0,
child: Container(
decoration: BoxDecoration(
color: Colors.purple,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'1:1',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
)
IntrinsicHeight & IntrinsicWidth
IntrinsicHeight sizes its child to the child’s intrinsic height, and IntrinsicWidth does the same for width. These are useful when you need siblings to match each other’s size but the layout system doesn’t do it automatically.
IntrinsicHeight for Equal-Height Cards
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Card(
color: Colors.blue[50],
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Short text'),
),
),
),
Expanded(
child: Card(
color: Colors.green[50],
child: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'This card has much more content which '
'makes it taller. The other card will '
'stretch to match this height.',
),
),
),
),
],
),
)
IntrinsicHeight and IntrinsicWidth perform a speculative layout pass, measuring the child twice. Use them sparingly—they can cause O(n²) performance when nested. Prefer other layout strategies (like Table or CrossAxisAlignment.stretch) when possible.
Practical Example: Constrained Input Field
Constrained Text Input
class ConstrainedInputField extends StatelessWidget {
const ConstrainedInputField({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 200,
maxWidth: 500,
),
child: TextField(
decoration: InputDecoration(
labelText: 'Email Address',
hintText: 'Enter your email',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.email),
),
),
),
);
}
}
Practical Example: Aspect-Ratio Video Player
Video Player with AspectRatio
class VideoPlayerCard extends StatelessWidget {
final String title;
final String thumbnailUrl;
const VideoPlayerCard({
super.key,
required this.title,
required this.thumbnailUrl,
});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
thumbnailUrl,
fit: BoxFit.cover,
),
Container(
color: Colors.black26,
child: const Center(
child: Icon(
Icons.play_circle_fill,
color: Colors.white,
size: 56,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}
ConstrainedBox to add min/max bounds, FittedBox to scale content into a space, AspectRatio to maintain proportional sizing, and IntrinsicHeight/Width only when no other layout widget achieves the desired result. Together, these widgets give you fine-grained control over how widgets size themselves within Flutter’s constraint-based layout system.