Flutter Wrap Widget: Chips, Tags, and Auto-Wrapping Layouts

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter Wrap widget guide, with chips and tags auto-wrapping across runs, spacing, and runSpacing visuals.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter Wrap widget guide.

The moment you build a row of tags or filter chips, a plain Row betrays you — the children spill off the edge and Flutter paints the yellow-and-black overflow stripes. Wrap is the fix: it lays children out like a Row, but when the next child won't fit, it flows onto a new line instead of overflowing. This guide covers how Wrap works, the spacing and runSpacing gaps, alignment across and within runs, a real chip layout, and the one constraint that trips people up — with paste-ready code.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Every snippet below is paste-ready against current stable Flutter. Wrap sits right next to Row and Column in your toolkit — reach for it whenever the count or width of children is dynamic.

Follow me on Instagram@sagnikteaches

We'll start with a basic Wrap, add the two kinds of spacing, control alignment across and within the runs, build a tag layout, and finish with when Wrap fails to wrap.

Connect on LinkedInSagnik Bhattacharya

If you'd rather watch chips and tag inputs built on screen, the channel walks through real, responsive Flutter UIs.

Subscribe on YouTube@codingliquids

The basic Wrap

Wrap takes a list of children and lays them along the main axis — horizontal by default. When the next child would exceed the available width, it starts a new run (a new line) rather than overflowing.

Wrap(
  children: [
    Chip(label: Text('Dart')),
    Chip(label: Text('Flutter')),
    Chip(label: Text('Firebase')),
    Chip(label: Text('Riverpod')),
    Chip(label: Text('Animations')),
  ],
)

Drop the same children into a Row on a narrow screen and you'd get an overflow error. Wrap simply flows them onto as many lines as it needs — which is exactly what you want for content whose size you can't predict.

spacing and runSpacing

By default the children are flush against each other, which looks cramped. Wrap gives you two independent gaps: spacing is the gap between children in the same run, and runSpacing is the gap between the runs.

Wrap(
  spacing: 8,      // horizontal gap between chips in a row
  runSpacing: 12,  // vertical gap between the wrapped lines
  children: tags.map((t) => Chip(label: Text(t))).toList(),
)

Forgetting runSpacing is the most common slip — the chips look fine on one line, then collapse together vertically the moment they wrap. Set both whenever you build a multi-line chip or tag layout.

Alignment across and within runs

Wrap has three alignment knobs. alignment positions children along the main axis within each run (using WrapAlignment), runAlignment positions the runs themselves along the cross axis, and crossAxisAlignment (a WrapCrossAlignment) aligns children of differing heights within a single run.

Wrap(
  alignment: WrapAlignment.center,
  runAlignment: WrapAlignment.start,
  crossAxisAlignment: WrapCrossAlignment.center,
  spacing: 8,
  runSpacing: 8,
  children: chips,
)

WrapAlignment offers the same family you know from Row and Columnstart, end, center, spaceBetween, spaceAround, and spaceEvenly — but applied per run, which is what makes centred, evenly distributed chip clouds easy.

The Complete Flutter Guide course thumbnail

Build real, responsive Flutter layouts

The Complete Flutter Guide takes you from layout fundamentals to shipped, adaptive apps step by step.

Enrol now

A real tag layout

Tags built from a list of strings are the canonical use case. Map each string to a styled container and let Wrap flow them onto as many lines as the screen allows.

Wrap(
  spacing: 8,
  runSpacing: 8,
  children: [
    for (final tag in interests)
      Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: Colors.deepPurple.shade50,
          borderRadius: BorderRadius.circular(20),
        ),
        child: Text(tag),
      ),
  ],
)

Because the layout is data-driven, you never have to worry about how many tags arrive or how long each one is — the same widget handles three short tags or thirty long ones. For interactive filters, swap the Container for a FilterChip or ChoiceChip from the Material catalogue.

Going vertical

Set direction: Axis.vertical and Wrap stacks children top to bottom, starting a new column when one fills the available height. The meaning of the two gaps swaps: spacing becomes the vertical gap within a column and runSpacing the horizontal gap between columns. The vertical form needs a bounded height to know when to wrap.

When Wrap fails to wrap

Wrap can only wrap if it knows where the edge is — it needs a bounded width (or height, when vertical). Place it inside something that hands it unbounded width — a horizontal ListView, or a Row without an Expanded around it — and it lays everything on one infinitely wide run, then overflows. Give it a bounded width with Expanded, a SizedBox, or simply by letting it take the full screen width, and it wraps as expected.

Wrap vs Row

Use a Row when you have a small, fixed set of children that you know will fit — a leading icon, a title, a trailing button. Reach for Wrap the moment the children are dynamic in count or width: tags, chips, attachment thumbnails, selectable options. The rule of thumb: if a designer could add "one more" of something and break your Row, it should have been a Wrap.

Common mistakes

  • Using Row for tags. Dynamic content overflows; Wrap flows onto new lines.
  • Setting spacing but not runSpacing. Wrapped lines collapse together vertically.
  • Wrap inside unbounded width. A horizontal ListView gives infinite width, so it never wraps.
  • Confusing alignment and runAlignment. One aligns within a run, the other aligns the runs.
  • Reaching for Wrap when a Row fits. For a fixed, known set, Row is simpler and clearer.

Frequently asked questions

What is the difference between Row and Wrap in Flutter?

A Row keeps everything on one line and overflows if it doesn't fit; Wrap moves the next child to a new run instead. Use Wrap for dynamic content like tags and chips.

What is the difference between spacing and runSpacing in Flutter Wrap?

spacing is the gap between children in the same run; runSpacing is the gap between the runs. Set both for multi-line chip layouts.

Can Wrap go vertical in Flutter?

Yes — set direction: Axis.vertical. Children stack into columns, and the two gaps swap meaning. The vertical form needs a bounded height.

Why does Wrap overflow or fail to wrap in Flutter?

It needs a bounded width to know when to start a new run. Inside unbounded width — a horizontal ListView, for instance — it never wraps. Constrain its width and it works.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — Wrap, WrapAlignment, WrapCrossAlignment, Axis, and Chip API references, plus the layout and constraints guides (docs.flutter.dev). Verified against current stable Flutter.