Wrap
Understand how to create flexible, multi-line layouts with Wrap.
What is it?
Wrap is a layout widget that arranges its children in a horizontal or vertical sequence and automatically wraps them to the next line or column when there isn't enough space. Unlike Row or Column, which try to fit all children in one direction, Wrap adapts to available space by creating multiple rows or columns as needed.
Why does it exist?
Wrap exists to:
- Create flexible, responsive layouts
- Handle multiple children that need to wrap
- Automatically break to new lines when space runs out
- Support chips, tags, and flow layouts
- Create responsive UI without manual calculations
- Handle dynamic content that changes size
Basic Wrap
Wrap arranges children and wraps to new lines.
// Basic Wrap usage
class BasicWrap extends StatelessWidget {
const BasicWrap({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8, // Horizontal spacing between children
runSpacing: 8, // Vertical spacing between rows
children: [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('Mobile')),
Chip(label: Text('Web')),
Chip(label: Text('Desktop')),
Chip(label: Text('iOS')),
Chip(label: Text('Android')),
Chip(label: Text('Development')),
],
);
}
}
// Wrap properties:
// 1. children - List of widgets
// 2. spacing - Space between children (horizontal)
// 3. runSpacing - Space between rows/columns (vertical)
// 4. direction - Axis.horizontal or Axis.vertical
// 5. alignment - How to align children
// 6. runAlignment - How to align rows/columns
// 7. crossAxisAlignment - How to align across axis
What's happening here? - Children arranged horizontally - Wrap to next line when space runs out - spacing controls horizontal gap - runSpacing controls vertical gap - Adapts to available space
Wrap Direction
Direction controls whether Wrap arranges horizontally or vertically.
// Wrap direction examples
class WrapDirectionExample extends StatelessWidget {
const WrapDirectionExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 1. Horizontal (default)
Container(
width: 200,
color: Colors.grey[200],
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildChip('Item 1'),
_buildChip('Item 2'),
_buildChip('Item 3'),
_buildChip('Item 4'),
_buildChip('Item 5'),
],
),
),
const SizedBox(height: 20),
// 2. Vertical
Container(
height: 150,
color: Colors.grey[200],
child: Wrap(
direction: Axis.vertical,
spacing: 8,
runSpacing: 8,
children: [
_buildChip('Item 1'),
_buildChip('Item 2'),
_buildChip('Item 3'),
_buildChip('Item 4'),
_buildChip('Item 5'),
_buildChip('Item 6'),
_buildChip('Item 7'),
_buildChip('Item 8'),
],
),
),
],
);
}
Widget _buildChip(String label) {
return Chip(
label: Text(label),
backgroundColor: Colors.blue[100],
);
}
}
What's happening here? - Horizontal wraps to next row - Vertical wraps to next column - Direction controls wrap behavior - spacing controls along main axis - runSpacing controls along cross axis
Wrap Alignment
Alignment controls how children are positioned.
// Wrap alignment examples
class WrapAlignmentExample extends StatelessWidget {
const WrapAlignmentExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 1. Start aligned
_buildWrapWithTitle(
'Start',
alignment: WrapAlignment.start,
),
// 2. Center aligned
_buildWrapWithTitle(
'Center',
alignment: WrapAlignment.center,
),
// 3. End aligned
_buildWrapWithTitle(
'End',
alignment: WrapAlignment.end,
),
// 4. SpaceBetween
_buildWrapWithTitle(
'SpaceBetween',
alignment: WrapAlignment.spaceBetween,
),
// 5. SpaceAround
_buildWrapWithTitle(
'SpaceAround',
alignment: WrapAlignment.spaceAround,
),
// 6. SpaceEvenly
_buildWrapWithTitle(
'SpaceEvenly',
alignment: WrapAlignment.spaceEvenly,
),
],
);
}
Widget _buildWrapWithTitle(String title, {required WrapAlignment alignment}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
Container(
width: 300,
color: Colors.grey[200],
child: Wrap(
alignment: alignment,
spacing: 8,
runSpacing: 8,
children: [
_buildChip('Chip A'),
_buildChip('Chip B'),
_buildChip('Chip C'),
_buildChip('Chip D'),
_buildChip('Chip E'),
],
),
),
const SizedBox(height: 10),
],
);
}
Widget _buildChip(String label) {
return Chip(label: Text(label));
}
}
// Alignment options:
// 1. WrapAlignment.start - Align to start
// 2. WrapAlignment.center - Center align
// 3. WrapAlignment.end - Align to end
// 4. WrapAlignment.spaceBetween - Space between
// 5. WrapAlignment.spaceAround - Space around
// 6. WrapAlignment.spaceEvenly - Even space
What's happening here? - start aligns children to beginning - center centers children - end aligns to end - spaceBetween distributes space - spaceAround spaces around each - spaceEvenly distributes evenly
Run Alignment
RunAlignment controls how rows/columns are aligned.
// RunAlignment examples
class RunAlignmentExample extends StatelessWidget {
const RunAlignmentExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 1. Start
_buildWrapWithRunTitle(
'Start',
runAlignment: WrapAlignment.start,
),
// 2. Center
_buildWrapWithRunTitle(
'Center',
runAlignment: WrapAlignment.center,
),
// 3. End
_buildWrapWithRunTitle(
'End',
runAlignment: WrapAlignment.end,
),
// 4. SpaceEvenly
_buildWrapWithRunTitle(
'SpaceEvenly',
runAlignment: WrapAlignment.spaceEvenly,
),
],
);
}
Widget _buildWrapWithRunTitle(
String title, {
required WrapAlignment runAlignment,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
Container(
width: 200,
height: 150,
color: Colors.grey[200],
child: Wrap(
runAlignment: runAlignment,
spacing: 8,
runSpacing: 8,
children: [
_buildChip('A'),
_buildChip('B'),
_buildChip('C'),
_buildChip('D'),
_buildChip('E'),
_buildChip('F'),
_buildChip('G'),
_buildChip('H'),
_buildChip('I'),
_buildChip('J'),
],
),
),
const SizedBox(height: 10),
],
);
}
Widget _buildChip(String label) {
return Chip(label: Text(label));
}
}
What's happening here? - runAlignment controls row alignment - start aligns rows to top - center centers rows - end aligns rows to bottom - spaceEvenly distributes rows
Wrap with Different Sizes
Wrap handles children of varying sizes.
// Wrap with varying children
class WrapWithVaryingSizes extends StatelessWidget {
const WrapWithVaryingSizes({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
// Different size chips
Chip(
label: const Text('Small'),
backgroundColor: Colors.blue[100],
),
Chip(
label: const Text('Medium Chip'),
backgroundColor: Colors.green[100],
),
Chip(
label: const Text('Very Long Label'),
backgroundColor: Colors.red[100],
),
Chip(
label: const Text('XL'),
backgroundColor: Colors.yellow[100],
),
Chip(
label: const Text('Medium Plus'),
backgroundColor: Colors.purple[100],
),
Chip(
label: const Text('Short'),
backgroundColor: Colors.orange[100],
),
],
);
}
}
What's happening here? - Children of different sizes - Wrap adapts to each child's size - Larger children take more space - Wrap handles varied content - Responsive to content changes
Real-World Examples
Common patterns using Wrap.
// 1. Tag cloud
class TagCloud extends StatelessWidget {
const TagCloud({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildTag('Flutter', 24),
_buildTag('Dart', 18),
_buildTag('Mobile', 20),
_buildTag('Web', 16),
_buildTag('Development', 22),
_buildTag('iOS', 14),
_buildTag('Android', 20),
_buildTag('UI', 12),
_buildTag('UX', 14),
_buildTag('Design', 18),
_buildTag('Programming', 22),
_buildTag('Framework', 16),
],
);
}
Widget _buildTag(String label, double size) {
return Chip(
label: Text(
label,
style: TextStyle(fontSize: size),
),
backgroundColor: Colors.blue[100],
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
);
}
}
// 2. Filter chips
class FilterChips extends StatefulWidget {
const FilterChips({super.key});
@override
State<FilterChips> createState() => _FilterChipsState();
}
class _FilterChipsState extends State<FilterChips> {
List<String> selectedFilters = [];
final List<String> filters = [
'All',
'Flutter',
'Dart',
'Mobile',
'Web',
'Desktop',
'iOS',
'Android',
'Backend',
'Frontend',
'Full Stack',
'DevOps',
];
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: filters.map((filter) {
final isSelected = selectedFilters.contains(filter);
return FilterChip(
label: Text(filter),
selected: isSelected,
onSelected: (selected) {
setState(() {
if (selected) {
selectedFilters.add(filter);
} else {
selectedFilters.remove(filter);
}
});
},
selectedColor: Colors.blue[200],
backgroundColor: Colors.grey[200],
);
}).toList(),
);
}
}
// 3. Image gallery with labels
class ImageGallery extends StatelessWidget {
const ImageGallery({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 16,
runSpacing: 16,
children: [
_buildGalleryItem('Image 1', 'assets/image1.jpg'),
_buildGalleryItem('Image 2', 'assets/image2.jpg'),
_buildGalleryItem('Image 3', 'assets/image3.jpg'),
_buildGalleryItem('Image 4', 'assets/image4.jpg'),
_buildGalleryItem('Image 5', 'assets/image5.jpg'),
],
);
}
Widget _buildGalleryItem(String title, String imagePath) {
return Container(
width: 150,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Container(
height: 100,
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: const Center(child: Icon(Icons.image, size: 40)),
),
Padding(
padding: const EdgeInsets.all(8),
child: Text(title),
),
],
),
);
}
}
// 4. Category selector
class CategorySelector extends StatelessWidget {
const CategorySelector({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildCategory('All', true),
_buildCategory('Flutter', false),
_buildCategory('Dart', false),
_buildCategory('Mobile', false),
_buildCategory('Web', false),
_buildCategory('Desktop', false),
_buildCategory('iOS', false),
_buildCategory('Android', false),
],
);
}
Widget _buildCategory(String label, bool isSelected) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey[200],
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey[300]!,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black87,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
);
}
}
What's happening here? - Tag cloud with varying sizes - Interactive filter chips - Image gallery with Wrap - Category selector chips - Common real-world patterns
Best Practices
Use Wrap for Dynamic Content
// Good - Wrap for dynamic chips
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: tags.map((tag) => Chip(label: Text(tag))).toList(),
);
}
// Bad - Using Row for dynamic content
@override
Widget build(BuildContext context) {
return Row(
children: tags.map((tag) => Chip(label: Text(tag))).toList(),
// Will overflow if too many tags
);
}
Set Appropriate Spacing
// Good - Proper spacing
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8, // Horizontal gap
runSpacing: 8, // Vertical gap
children: children,
);
}
Use CrossAxisAlignment for Alignment
// Good - CrossAxisAlignment control
@override
Widget build(BuildContext context) {
return Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: children,
);
}
Common Mistakes
No Spacing
Wrong:
// No spacing between children
Wrap(
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
],
)
Correct:
// Add spacing
Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('A')),
Chip(label: Text('B')),
Chip(label: Text('C')),
],
)
Wrong Direction
Wrong:
// Using horizontal for vertical chips
Wrap(
direction: Axis.horizontal,
children: [
Chip(label: Text('Long text that will wrap')),
// Might not wrap as expected
],
)
Correct:
// Use appropriate direction for content
Wrap(
direction: Axis.vertical,
spacing: 8,
children: [
Chip(label: Text('Long text that will wrap')),
],
)
Summary
Wrap arranges children in rows or columns and automatically wraps to new lines when space runs out. Use Wrap for chips, tags, categories, and any layout with dynamic content that needs to adapt to available space.
Next Steps
Did You Know?
- Wrap is like a flexible Row/Column
- Wrap automatically creates new rows/columns
- spacing controls horizontal gaps
- runSpacing controls vertical gaps
- Wrap handles children of different sizes
- Wrap supports both directions
- Wrap is perfect for responsive chips
- Wrap adapts to content changes