Top 10 Best Flutter List Tile Widgets: Patterns, Variants and M3 Migration (2026)

Top 10 Flutter ListTile widgets for clean rows with leading and trailing icons, titles, and subtitles — with code examples and GetWidget's GFListTile.

Stacked horizontal row primitives composing a list interface, editorial illustration

Across the ten industries we ship Flutter in, ListTile shows up everywhere a settings screen, a chat thread, a notification feed, or any list of selectable rows needs to render. The choice between Flutter's built-in ListTile, one of its variants (CheckboxListTile, SwitchListTile, RadioListTile, ExpansionTile), a community package, or a hand-rolled Row looks small at design time and turns into an accessibility, performance, or theming problem once the app hits real users. This guide walks through the flutter list tile widget patterns we actually ship, when each one earns its place, the Material 3 defaults that changed under teams who upgraded without checking, and the production-grade fixes that most ListTile tutorials skip.

Two framing notes. Pub.dev currently lists more than 40 actively maintained ListTile-adjacent packages. Most of them solve the same problem with cosmetic variations. The patterns worth knowing solve something the built-in ListTile cannot do cleanly: expandable rows with state retention, swipe-dismissible rows, persistent timeline-style rows, or virtualised long lists where every frame matters. The rest is taste. Second: Material 3 changed enough ListTile defaults (text styles, leading-trailing alignment, state-driven colors) that a useMaterial3: true flip on an existing app will visibly shift every list. Read the M3 section before you upgrade.

When ListTile is the right primitive (and when it isn't)

ListTile ships with Flutter. It takes a leading widget, a title, an optional subtitle, a trailing widget, and an onTap callback. For 80% of row-style UI (settings screens, contact lists, notification feeds, message previews) it is the right answer. No package, no extra dependency, no custom layout math. The reason to reach for anything else is almost always one of four specific needs: a stateful variant (checkbox or switch or radio), an expandable row, swipe-to-dismiss, or a layout the ListTile padding and height contract simply cannot reach.

The pattern that bites teams: skipping ListTile and writing a custom Row with Padding to get pixel-exact spacing. That custom row almost always loses the 48dp minimum tap target, the built-in InkWell ripple, the implicit Semantics that names this row to a screen reader, and the dense / visualDensity hooks that let the system shift heights for accessibility settings. We have audited apps where every settings row was a hand-rolled Row, and re-fixing accessibility cost more than the original layout work would have.

NeedUseWhy
Settings row, contact row, notification rowListTile48dp tap target, ripple, Semantics, theming — all free
Row with a toggle that updates stateSwitchListTileWhole row is the tap target, not just the switch
Row with multiselect checkboxCheckboxListTileSame logic: whole row is tappable, with M3 Checkbox defaults
Single-choice option in a settings groupRadioListTileGroup state is managed via groupValue; preserves a11y
Expandable section header with children belowExpansionTileBuilt-in expand/collapse animation and state retention
Pixel-exact custom layout that ListTile cannot reachInkWell + Row inside MaterialKeep the ripple and tap target; lose ListTile's contract intentionally
Row inside a Card with raised visual treatmentCard with ListTile childCard handles elevation; ListTile handles structure
Reach for ListTile vs custom Row vs a variant

The ListTile anatomy our review checklist uses

ListTile has three horizontal slots (leading, center, trailing) and a vertical contract that depends on how many lines of text it holds. One-line: 56dp tall, leading widget centered vertically. Two-line: 72dp tall, title and subtitle stacked. Three-line: 88dp tall, requires isThreeLine: true explicitly. Most layout bugs we see in code review come from passing two paragraphs into subtitle without isThreeLine, which causes a RenderFlex overflow at the bottom on smaller fonts.

lib/widgets/settings_row.dart
DART
ListTile(
  leading: const Icon(Icons.notifications_outlined),
  title: const Text('Notifications'),
  subtitle: const Text('Push, email, in-app'),
  trailing: const Icon(Icons.chevron_right),
  onTap: () => context.push('/settings/notifications'),
  // Make the whole row a single Semantics node, not five children.
  // Without this, screen readers announce each leading/trailing icon
  // separately and lose the row context.
);

Two properties that earn their keep on every project: contentPadding (override the 16dp horizontal default when nesting inside a Card with its own padding, otherwise rows look indented by 32dp), and minLeadingWidth (force-align leading icons across mixed rows where some have avatars and some have small icons). We set these in ListTileTheme at the app level so individual screens stay clean.

Material 3 changed these ListTile defaults — read before you flip the flag

Setting useMaterial3: true on a ThemeData built around the Material 2 ListTile contract will shift every list visibly. Six things change. First, three new text style properties (titleTextStyle, subtitleTextStyle, leadingAndTrailingTextStyle) replace the M2-era pattern of styling text inside the title widget. Second, the new titleAlignment property controls how leading and trailing widgets align relative to a multi-line title — three options (threeLine, titleHeight, center), where titleHeight is the new M3 default and looks different from M2's center. Third, textColor and iconColor now accept WidgetStateColor, so you can drive selected and disabled state colors from one theme entry rather than four overrides.

lib/theme/list_tile_theme.dart
DART
ThemeData(
  useMaterial3: true,
  listTileTheme: ListTileThemeData(
    titleTextStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
    subtitleTextStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
    leadingAndTrailingTextStyle: const TextStyle(fontSize: 14),
    titleAlignment: ListTileTitleAlignment.threeLine,
    iconColor: WidgetStateColor.resolveWith((states) {
      if (states.contains(WidgetState.selected)) return Colors.deepPurple;
      if (states.contains(WidgetState.disabled)) return Colors.grey;
      return Colors.black87;
    }),
    minLeadingWidth: 32,
  ),
);

Three more shifts that catch teams. The selected state under M3 paints the row with the secondaryContainer color rather than the M2 grey overlay, so a designer comparing screenshots will flag the change immediately. ListTile inside a Card now inherits the card's surface tint color in its hover state, which is invisible on most screens but ships with a faint M3 elevation tint. And the dense: true shortcut still works but is functionally equivalent to visualDensity: VisualDensity.compact under M3 — pick visualDensity for forward-compatible code.

Top 10 best flutter list tile widget patterns we ship

The patterns and packages below are what we reach for on real client builds. Order reflects how often each one earns its place across the Flutter apps we ship in healthcare, fintech, ecommerce, and the other seven industries on our books. Maintenance status reflects pub.dev as of May 2026.

1. ListTile (built-in): the default for 80% of rows

Built-in ListTile with a theme-level ListTileThemeData is the highest-impact pattern in this list. It ships with the right tap target, the right ripple, the right Semantics, and the right height contract. The one rule we enforce in code review: if a row would have been a hand-rolled Row, justify why ListTile cannot reach the design before approving the PR.

2. CheckboxListTile: multiselect rows without losing tap targets

CheckboxListTile makes the entire row the tap target, not just the 24dp checkbox. We use it on filter screens and permission pickers, plus the multiselect contact pickers in CRM-style apps. The controlAffinity prop (leading vs trailing vs platform) lets the same code render the checkbox where each platform expects it — iOS users expect trailing, Material users expect leading.

3. SwitchListTile: settings rows where the whole row toggles

SwitchListTile is the right widget for every notification preference, dark-mode toggle, and feature flag in a settings screen. Same tap-target argument as CheckboxListTile. Under M3 the switch ships with the redesigned track-and-thumb visual; the Switch.adaptive variant of the standalone widget exists but SwitchListTile itself does not yet expose adaptive — render Cupertino settings with a manual swap if iOS parity matters.

4. RadioListTile: single-choice from a group

RadioListTile manages group state via groupValue and value props. It is the right widget for currency pickers, theme pickers, and any single-choice settings group. The accessibility win: a screen reader announces the row as 'Selected, USD' rather than 'Radio button, USD', because RadioListTile sets the right Semantics.checked flag at the row level.

5. ExpansionTile: collapsible section headers with state retention

ExpansionTile gives you a ListTile-styled header with an animated expand-collapse, and children rendered below when open. We use it for FAQ screens, grouped settings, and any content where a user scans a list of section titles and drills into one. Two production tips: pass a controller (ExpansionTileController) if you need programmatic open or close, and set maintainState: true if the children hold form state — without it, ExpansionTile rebuilds children on every collapse and loses TextField content.

6. GFListTile (GetWidget): themed default for apps already on GetWidget

GFListTile is part of the open-source GetWidget UI Kit we maintain (4,811 stars, 23K monthly pub.dev downloads). It exposes size presets, a built-in description slot, and theming hooks that match the rest of GetWidget. Pull it in when you are already using GetWidget components elsewhere in the app. For one-off list rendering, built-in ListTile plus a ListTileThemeData is the lower-overhead choice.

7. Dismissible-wrapped ListTile: swipe-to-archive rows

Wrap a ListTile in Dismissible to get a Gmail-style swipe-to-archive or swipe-to-delete row. The pattern is one widget, but the gotchas are real: every Dismissible needs a stable key (use the row's record id, not the index), confirmDismiss should prompt the user before a destructive action, and the onDismissed callback must remove the item from the source list before the next build or the framework throws a duplicate-key exception.

8. flutter_expanded_tile: persistent expanded state across rebuilds

Built-in ExpansionTile loses expand state when the parent rebuilds the tree (a common gotcha with StreamBuilder and FutureBuilder above the list). flutter_expanded_tile ships an ExpandedTileController that survives parent rebuilds because state lives outside the widget. We pull this package on apps where the list is driven by a stream that emits often (a chat thread, a live order feed).

9. timeline_tile: vertical timelines with consistent rail alignment

Order-tracking screens, audit logs, and activity feeds want a vertical rail with dots and connectors between rows — a ListTile cannot model that cleanly. timeline_tile renders the rail, the indicator, and the row content in one widget with consistent alignment math. Two trade-offs: it is heavier than ListTile, and accessibility is on you (the rail is decorative; mark it Semantics(excludeSemantics: true)).

10. Custom Row inside Material + InkWell: when ListTile cannot reach the design

When the design needs more than three columns, an irregular height, or a layout ListTile cannot model, the right escape is not 'add another package'. Build a custom Row inside a Material widget, wrap it with InkWell for the ripple, set a fixed constraints with BoxConstraints(minHeight: 48), and wrap the result in Semantics with a button: true label. That gives you ListTile-equivalent tap targets and a11y without the layout contract.

Picking between ListTile variants: a decision matrix

The four ListTile variants (Checkbox, Switch, Radio, Expansion) cover most stateful-row designs. The mistake we see most often: nesting a plain Switch inside a ListTile's trailing slot instead of using SwitchListTile, which means only the 36dp switch is tappable rather than the full 360dp row. The matrix below is how we pick the variant in design review.

Use case ListTile + manual controlCheckboxListTileSwitchListTileRadioListTileExpansionTile
Multiselect contact picker Avoid: loses tap target Yes: best fit No No No
Notification on/off settings row Avoid No Yes: best fit No No
Currency picker (single choice) Avoid No No Yes: best fit Inside ExpansionTile if grouped
FAQ section with collapsible answer No No No No Yes: best fit
Chat thread row (avatar, name, preview, time) Yes: best fit No No No No
Settings row with disclosure arrow Yes — best fit (trailing: chevron) No No No If row also contains sub-options

ListTileTheme: getting consistent styling across the app

Almost every ListTile property has a corresponding ListTileThemeData entry. Set them once in ThemeData.listTileTheme and every list in the app inherits, including ListTiles inside Card and Drawer widgets and inside BottomSheet or Dialog instances. The biggest gain: when design tweaks the leading icon color from #555 to #444, you change one line in the theme rather than 47 ListTile call sites.

lib/theme/app_theme.dart
DART
ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
  listTileTheme: ListTileThemeData(
    iconColor: Colors.black87,
    textColor: Colors.black87,
    contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
    minLeadingWidth: 32,
    selectedColor: Colors.deepPurple,
    selectedTileColor: Colors.deepPurple.withOpacity(0.08),
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(12)),
    ),
  ),
  // For one-off overrides on a specific screen, wrap the list
  // in ListTileTheme(...) — it inherits unset values from above.
);

Two properties that move the needle on visual polish: shape (round the row corners under M3 to match Card and BottomSheet radii — 12dp is the M3 default for medium containers), and selectedTileColor (the M3 secondaryContainer fill at low opacity is the right visual cue for a selected row). Both are set once at the theme level and propagate everywhere.

Performance: ListTile in long lists is where Flutter apps jank

A scrolling list of ListTile widgets is one of the most common sources of frame-time blowouts in Flutter apps we audit. The widget itself is cheap. The problem is what teams put inside it — an uncached NetworkImage in the leading slot, a heavyweight build method in title, a StreamBuilder that rebuilds the entire row when one field changes.

lib/screens/contacts.dart
DART
ListView.builder(
  itemCount: contacts.length,
  itemBuilder: (context, index) {
    final c = contacts[index];
    return RepaintBoundary(
      child: ListTile(
        leading: CircleAvatar(
          backgroundImage: CachedNetworkImageProvider(c.photoUrl),
        ),
        title: Text(c.name),
        subtitle: Text(c.lastMessage),
        trailing: const Icon(Icons.chevron_right),
        onTap: () => context.push('/chat/${c.id}'),
      ),
    );
  },
);

Accessibility: what ListTile gives you and what it doesn't

ListTile is the most accessible row primitive in Flutter. It enforces a 48dp minimum tap target, emits a single Semantics node per row by default, announces the row as a button when onTap is set, and inherits the system's text scaling. That is more than most custom Row implementations ship with. The three gaps that catch teams:

GapSymptomFix
Decorative leading icons announced separatelyScreen reader reads icon name (e.g. 'Notifications icon') before the row titleWrap leading in ExcludeSemantics, or set MergeSemantics on the row parent
Trailing chevron read as 'Chevron right button'Two interactive announcements for one tap targetPass null for trailing's Semantics or wrap in ExcludeSemantics
Selected state not announcedScreen reader does not say 'selected' on the current rowUse ListTile selected: true and set selectedColor; the Semantics flag follows
Three-line content overflowText clips silently at small font scalesSet isThreeLine: true; do not rely on subtitle to render two paragraphs
Accessibility gaps in default ListTile usage

The simplest accessibility upgrade on most Flutter apps we audit: wrap every list with MergeSemantics at the row level and ExcludeSemantics on every decorative icon. Two lines of code per row, and the screen-reader experience moves from unusable to acceptable in one pass.

The five ListTile bugs we see in code review every month

BugWhat goes wrongFix
RenderFlex overflow on small fontsTwo-paragraph subtitle clips at the bottom at text-scale 1.3 and aboveSet isThreeLine: true on the ListTile
dense vs visualDensity confusiondense: true silently sets visualDensity to compact, overriding theme defaultsPick one: drop dense in favor of explicit visualDensity in the theme
Infinite-height layout exceptionListTile inside a Column without a SingleChildScrollView throws unbounded constraint errorsWrap in Expanded or use ListView; never put ListTile in a free Column for more than one row
Switch / Checkbox tap target collapsesPlain Switch in trailing slot only makes the 36dp switch tappable, not the rowReplace with SwitchListTile or CheckboxListTile
onTap fires on subtitle Text long-pressCustom GestureDetector inside title or subtitle interferes with ListTile's gesture handlingMove the gesture handler to ListTile's onLongPress; do not nest GestureDetectors
Recurring ListTile bugs and the one-line fixes

Migrating from Material 2 ListTile to Material 3

If you are flipping useMaterial3: true on an existing app, walk this checklist before merging. The first four items move screenshots; the last two prevent silent regressions in theming behavior.

M2 patternM3 replacementNotes
TextStyle inside title: Text(style: ...)titleTextStyle on ListTileThemeDataSingle source of truth; survives theme switches
Custom selected highlight via Container wrapselectedColor + selectedTileColor on ListTileInherits M3 secondaryContainer; respects state
dense: truevisualDensity: VisualDensity.compactForward-compatible; future M4 may deprecate dense
Static iconColor for all statesWidgetStateColor.resolveWith on iconColorSelected, disabled, pressed states unified
Manual height via SizedBox wrappertitleAlignment + visualDensityLets the framework manage 56/72/88 dp contract
Trailing IconButton with own SemanticsTrailing Icon + ExcludeSemanticsOne Semantics node per row, not two
M2 to M3 ListTile migration checklist

For the leading-slot widget patterns that most often pair with ListTile — avatars, status indicators, icons — see our guide to flutter avatar widgets. For how list rows fit into a production Flutter app (state plus performance and navigation and CI/CD) ourFlutter mobile app development field guide covers the practices we enforce on every build.

Common questions about the Flutter list tile widget

What is the flutter list tile widget used for?

ListTile is a single-row primitive for any list of items with a leading slot, a title, an optional subtitle, and a trailing slot. It enforces a 48dp tap target, a built-in InkWell ripple, the right Semantics for screen readers, and the M3 height contract (56dp one-line, 72dp two-line, 88dp three-line). Most settings screens, contact lists, chat threads, and notification feeds use it as the row primitive.

When should I use CheckboxListTile instead of ListTile with a trailing Checkbox?

Whenever you want the whole row to toggle the checkbox, not just the 24dp checkbox itself. CheckboxListTile makes the entire row the tap target and routes the Semantics correctly, so a screen reader announces the row as 'Checked' or 'Not checked' rather than 'Checkbox button'. The same logic applies to SwitchListTile and RadioListTile.

Does Material 3 change how ListTile looks?

Yes. Flipping useMaterial3: true changes six things: three new text style properties (titleTextStyle, subtitleTextStyle, leadingAndTrailingTextStyle), the default titleAlignment shifts to the M3 titleHeight rule, textColor and iconColor accept WidgetStateColor for state-driven theming, the selected row paints with secondaryContainer rather than the M2 grey overlay, ListTile inside a Card picks up surface tint, and dense: true becomes equivalent to visualDensity: compact. Update screenshots before merging the M3 flip.

How do I make ListTile rounded?

Set shape on the ListTile or, better, on ListTileThemeData so every row in the app inherits. The M3 default for medium containers is 12dp; for tight settings rows we use 8dp. Combine with selectedTileColor to get a rounded selected highlight that matches the M3 Card and BottomSheet styling.

Why does my ListTile throw a layout exception inside Column?

ListTile expects bounded width but a free Column inside a Column or inside a Scaffold body can give unbounded vertical constraints. For more than one row, switch to ListView or ListView.builder. For a single row in a Column, wrap in Flexible or set a SizedBox with explicit height. Never put a long list of ListTile widgets in a non-scrolling Column — the framework will accept it on small lists and break later.

How do I improve ListTile performance in a long list?

Three rules. Use ListView.builder, not ListView with a children list, the moment the list crosses about 20 rows. Wrap each ListTile in a RepaintBoundary when rows hold images. const-ify every static child (const Icon, const Text). That combination gets us from 38fps to 60fps on a 500-row contact list on a mid-range Android device.

Is ListTile accessible by default?

Mostly. It emits a single Semantics node, enforces a 48dp tap target, and announces the row as a button when onTap is set. The three gaps to fix: wrap decorative leading icons in ExcludeSemantics, wrap trailing chevrons the same way, and set selected: true (not just a custom highlight color) so the screen reader announces the selected state. Two lines of code per row moves the screen-reader experience from unusable to acceptable.

What is the difference between ListTile and GFListTile?

ListTile ships with Flutter — no dependency, no bundle cost. GFListTile is part of the open-source GetWidget UI Kit we maintain and adds size presets, a built-in description slot, and theming hooks consistent with the rest of GetWidget. Pick GFListTile when you are already using GetWidget components elsewhere; pick built-in ListTile when list rows are an isolated need.

How do I make ExpansionTile remember its state across rebuilds?

Built-in ExpansionTile loses state when its parent rebuilds (a common gotcha with StreamBuilder or FutureBuilder above the list). Two options: pass an ExpansionTileController and manage state in a parent StatefulWidget, or use the flutter_expanded_tile package whose ExpandedTileController lives outside the widget tree. For chat threads and live feeds, the package is the lower-friction choice.

Part of the /Flutter App Development Company series.

RELATED

More reading.

top 10 best flutter avatar widgets list hero image
#flutter#avatar widget

Top 10 Best Flutter Avatar Widgets: A 2026 Practitioner Guide

Top 10 Flutter avatar widgets to render user images and initials in circular, square, and custom shapes — with code examples and GetWidget's GFAvatar.

Navin Sharma Navin Sharma
5m
Flutter Mobile App Development: A 2026 Production Field Guide — hero image
#flutter#mobile-development

Flutter Mobile App Development: A 2026 Production Field Guide

How we structure Flutter projects at GetWidget in 2026: feature-first layout, Riverpod defaults, Dart 3 records and sealed classes, Material 3 theming, the 200-line widget rule, performance diagnosis, CI/CD pipelines, and the production pitfalls that bite teams after launch.

Navin Sharma Navin Sharma
5m
best flutter drawer widgets hero image
#flutter#drawer widget

Best Flutter Drawer Widgets in 2026: Drawer, NavigationDrawer, and the Packages Still Worth Using

Top 10 Flutter drawer widgets for slide-out side menus: customization patterns, code examples, and how to integrate GetWidget's GFDrawer into your app.

Navin Sharma Navin Sharma
5m
flutter button widget component hero image
#flutter#flutter buttons

How to Design Custom Flutter Buttons in 2026: A Practitioner Guide to All 5 M3 Button Widgets

Flutter button widgets in 2026: the five Material 3 button classes (Filled, FilledTonal, Elevated, Outlined, Text), ButtonStyle deep-dive, GFButton when M3 isn't enough, FAB and IconButton patterns, M2 migration map, plus the accessibility and performance bars no tutorial covers.

Navin Sharma Navin Sharma
5m
Back to Blog