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.

flutter button widget component hero image

Buttons are the most heavily used widget in any production Flutter app, and the most quietly broken. Material 3 split the old single ElevatedButton role into a five-button hierarchy that communicates emphasis through visual weight, and the old M2 classes — FlatButton, RaisedButton, OutlineButton — are gone. The widget choice matters because Google now indexes Lighthouse-style accessibility signals, and buttons are usually where those signals leak. This guide walks through the five Flutter button widgets we ship across ten industries, the ButtonStyle API that controls every visual property, the GFButton from the GetWidget UI kit we maintain, and the production-grade patterns that no tutorial covers.

Two framing notes. The flutter button widget question every junior engineer asks — which button do I use here? — has a real answer that tracks Material 3's emphasis hierarchy, not random preference. And every flutter button widget in production has to clear three bars the framework defaults do not: minimum tap target, contrast against the surface behind it, and a Semantics label that a screen reader can announce. We cover all three.

The five Material 3 flutter button widget classes and when to pick each

Material 3 introduced an explicit emphasis hierarchy. Each button signals a different level of importance through visual weight: filled background, outlined border, raised shadow, or pure text. The order below is from highest emphasis (a single primary action per screen) to lowest (frequent low-stakes actions).

WidgetEmphasisVisual treatmentPick it when
FilledButtonHighestSolid filled backgroundPrimary action on a screen (Submit, Continue, Save)
FilledButton.tonalHighFilled with secondary colorImportant action that is not the single primary
ElevatedButtonMedium-highSolid + drop shadowFloating action surfaces; legacy M2 alignment
OutlinedButtonMediumBorder only, transparent fillSecondary actions, Cancel, Back
TextButtonLowestText only, no backgroundInline links, dialog actions, repeated row actions
Material 3 button hierarchy: emphasis, visual treatment, and primary use

Two M3 additions catch teams on migration. FilledButton (no elevation) replaces ElevatedButton as the recommended primary action; ElevatedButton still works but is no longer the default. FilledButton.tonal is a secondary-color filled variant that bridges the gap between FilledButton and OutlinedButton when one screen has two distinct levels of important action.

Flutter button widget syntax: the basic pattern across all five

Every M3 button class takes the same constructor shape: an onPressed callback, a child widget for the button label, and an optional style ButtonStyle for visual customization. Passing null to onPressed renders the button in a disabled state.

lib/widgets/primary_action.dart
DART
FilledButton(
  onPressed: isLoading ? null : _onSubmit,
  child: const Text('Save changes'),
);

FilledButton.tonal(
  onPressed: _onSecondary,
  child: const Text('Save and add another'),
);

OutlinedButton(
  onPressed: _onCancel,
  child: const Text('Cancel'),
);

TextButton(
  onPressed: _onSkip,
  child: const Text('Skip for now'),
);

The disabled state is worth thinking about explicitly. Setting onPressed to null is how Flutter knows a button cannot be tapped, and Material 3 styles the disabled state automatically (reduced opacity, no ink ripple, screen reader announces 'disabled'). Junior engineers sometimes hide the button entirely while waiting on a state change. That breaks expectations: the layout shifts, screen readers lose the element, and users do not know what to expect when the state resolves.

Adding icons: the .icon constructor every button class ships

Every M3 button class has a paired .icon constructor that adds a leading icon next to the label. Use this when the icon adds genuine recognition (Save icon next to Save, plus icon next to Add). Avoid decorative icons on buttons. They double the tap target's visual weight without helping the user act faster.

lib/widgets/action_with_icon.dart
DART
FilledButton.icon(
  onPressed: _onAdd,
  icon: const Icon(Icons.add),
  label: const Text('New project'),
);

OutlinedButton.icon(
  onPressed: _onExport,
  icon: const Icon(Icons.download),
  label: const Text('Export CSV'),
);

ButtonStyle: the one API that controls every visual property

Material 3 replaced the per-widget property soup of M2 (color, textColor, padding, shape, splashColor) with a single ButtonStyle object. Every property on ButtonStyle takes a MaterialStateProperty so the value can resolve differently based on hovered, pressed, focused, disabled state.

lib/widgets/branded_button.dart
DART
FilledButton(
  onPressed: _onAction,
  style: FilledButton.styleFrom(
    backgroundColor: brand.primary,
    foregroundColor: brand.onPrimary,
    minimumSize: const Size(120, 48),
    padding: const EdgeInsets.symmetric(horizontal: 24),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  child: const Text('Continue'),
);

The styleFrom factory is the friendlier surface most teams use day-to-day. For state-dependent styling (different background when hovered or pressed), drop down to ButtonStyle directly and pass MaterialStateProperty.resolveWith. We push teams to set button styles at the theme level rather than inline on every button — the cost of inline styles is hard to spot until you ship a redesign and need to find every button that overrode the theme.

Setting button themes once at the app level (the only way that scales)

Every button class has a paired theme: FilledButtonTheme, ElevatedButtonTheme, OutlinedButtonTheme, TextButtonTheme. Each takes a style of ButtonStyle. Setting these once in your ThemeData means every button in the app picks up brand defaults without per-widget overrides.

lib/theme.dart
DART
MaterialApp(
  theme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(seedColor: brand.primary),
    filledButtonTheme: FilledButtonThemeData(
      style: FilledButton.styleFrom(
        minimumSize: const Size(double.infinity, 48),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
    outlinedButtonTheme: OutlinedButtonThemeData(
      style: OutlinedButton.styleFrom(
        minimumSize: const Size(double.infinity, 48),
      ),
    ),
  ),
);

Setting minimumSize at the theme level is how we enforce the 48 dp tap-target rule across every button without remembering it on every widget. ColorScheme.fromSeed pulled into useMaterial3 mode means every button class picks up brand colors automatically — FilledButton uses primary, FilledButton.tonal uses secondaryContainer, OutlinedButton uses outline, TextButton uses primary text on transparent. You set the seed once and the cascade handles the rest.

GFButton: the GetWidget flutter button widget for cross-shape needs

GFButton ships with the open-source GetWidget UI kit we maintain (4,811 GitHub stars, 23K monthly pub.dev downloads). It exposes shape variants Material 3 does not ship natively — circular, pills with extreme rounding, square solid corners, social-network-branded styles. The constructor takes a type prop (solid, transparent, outline) and a shape prop (standard, pills, square, shadow) so you can describe a button in one line.

lib/widgets/social_button.dart
DART
GFButton(
  onPressed: _onGoogleSignIn,
  text: 'Sign in with Google',
  icon: const Icon(Icons.account_circle),
  shape: GFButtonShape.pills,
  type: GFButtonType.outline,
  fullWidthButton: true,
);

Pull GFButton in when your design system needs pills, distinctly rounded squares, or branded social buttons without re-implementing the styling on every screen. For straight M3 buttons in a typical app, the built-in FilledButton plus a themed style is the lower-overhead choice.

Performance: where buttons silently drag your frame rate

Three patterns we catch in code review on most Flutter teams ship without realizing the cost.

First: passing a non-const child to a const-able button. FilledButton(child: Text('Save')) without const forces Flutter to rebuild the Text widget on every parent rebuild. The fix is one keyword: const Text('Save'). Across a screen with twenty buttons, that single change has measurable impact in the DevTools rebuild counter.

Second: defining onPressed inline as an anonymous closure. () => _onAction() creates a new function instance on every rebuild, which breaks button equality checks and triggers downstream rebuilds in any widget that listens. Define the callback as a tear-off (just _onAction) when possible, or extract it to a stable method.

Accessibility: the three bars every flutter button widget has to clear

Material 3 buttons inherit some accessibility for free: minimum tap target hint, focus ring, ripple feedback, screen reader role. But three things still need explicit attention.

First, tap target. Material says 48x48 dp minimum. Flutter's default button height is 48 in most cases but you can break this by setting padding or minimumSize to smaller values. Enforce 48 dp at the theme level so no button ever ships below it.

Second, contrast. Buttons sit on top of surfaces. The label has to clear WCAG AA contrast against the button's background, and the button's background has to clear contrast against the surface behind it. ColorScheme.fromSeed picks values that pass for the standard tonal pairs; manually overriding colors is where contrast usually breaks.

Third, label clarity for screen readers. Buttons announce their child text by default, so 'Submit', 'Save', 'Continue' read fine. Icon-only buttons need a tooltip prop (which Flutter promotes to a Semantics label) or an explicit Semantics wrap. We require either tooltip or Semantics on every icon-only button in code review.

lib/widgets/icon_only_button.dart
DART
IconButton(
  tooltip: 'Save changes',
  icon: const Icon(Icons.save),
  onPressed: _onSave,
);

// Or for non-IconButton icon-only buttons:
Semantics(
  label: 'Save changes',
  button: true,
  child: FilledButton(
    onPressed: _onSave,
    child: const Icon(Icons.save),
  ),
);

FloatingActionButton and IconButton: the other flutter button widgets

The five M3 button classes cover most cases, but two other button-like widgets ship with Material and deserve their own pattern guidance. FloatingActionButton (FAB) is the primary action on a screen that floats above content — typically a create or compose action. Material recommends one FAB per screen, and the icon should be the verb (add, edit, send), not the noun. The size, foreground color, and elevation come from FloatingActionButtonTheme, set once in ThemeData.

IconButton is for icon-only actions that sit inside an AppBar, BottomAppBar, ListTile trailing slot, or any compact row where space rules out a full button. The widget has a built-in tooltip prop that Flutter promotes to the screen reader's semantic label — always set tooltip on every IconButton, even ones that 'look obvious' to a sighted user. M3 added IconButton variants (IconButton.filled, IconButton.filledTonal, IconButton.outlined) that mirror the FilledButton hierarchy when the icon needs more visual weight than a plain tap target.

One common mistake we catch in code review: using a plain Container with InkWell instead of a real button widget. The custom version skips the focus ring, tap target enforcement, Material ripple, and screen reader announcement. Every clickable widget in a Flutter app should be a button widget or wrapped in InkWell with explicit Semantics(button: true). Skipping this is how 'looks fine' apps fail accessibility audits.

Migrating from M2: FlatButton, RaisedButton, OutlineButton are gone

If you are upgrading a Flutter app written before 2022, you will hit deprecated and then removed button classes. The replacement map is one-to-one but the property names changed.

Old (M2)New (M3)Property change
FlatButtonTextButtoncolor → ButtonStyle.foregroundColor
RaisedButtonElevatedButton or FilledButtoncolor → ButtonStyle.backgroundColor
OutlineButtonOutlinedButtonborderSide → ButtonStyle.side
FlatButton.icon / RaisedButton.icon / OutlineButton.iconCorresponding .icon variantSame structure, new class
M2 button class → M3 replacement and main property changes

Two migration notes we enforce. First, swap FlatButton to TextButton where the M2 button was used for low-emphasis text actions, swap to OutlinedButton if it was wrapping a border manually. Second, replace inline property soups (color, textColor, padding, shape) with ButtonStyle in one pass per file; bouncing back and forth between old and new APIs in the same file invites bugs.

One last migration gotcha worth surfacing: the M2 buttons had a child for the label plus optional icon properties on .icon variants. The M3 button.icon constructors split this into icon (required) and label (required). If your IDE auto-converts old FlatButton.icon usages, double-check that the icon and label slots actually moved to the right parameters. We have seen migrations where the IDE refactor pasted both into the icon slot and the label rendered as a tiny icon-sized text node.

Buttons are one of the most-touched widget families in any Flutter app. For the broader Flutter widgets catalog we maintain across our deliveries, and how button styling fits into a complete production stack — state, performance, CI/CD, accessibility — see our Flutter mobile app development field guide.

Common questions about flutter button widgets

Which flutter button widget should I use for the primary action on a screen?

FilledButton. It is Material 3's highest-emphasis button and the default primary-action choice. ElevatedButton (with drop shadow) is the M2-style alternative if your design language relies on raised surfaces, but for new Flutter apps in 2026, FilledButton is the right default.

What is the difference between FilledButton and ElevatedButton?

Both are filled, but ElevatedButton sits above the surface with a drop shadow while FilledButton is flat. M3 treats FilledButton as the primary high-emphasis button; ElevatedButton is now reserved for cases where elevation communicates a floating surface (FAB-adjacent contexts).

How do I create a rounded button in Flutter?

Set shape on ButtonStyle to a RoundedRectangleBorder with the borderRadius you want. RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)) is a common default; circular(999) gives you full pills. Set this at the theme level (FilledButtonTheme) once and every button in the app picks it up.

How do I disable a flutter button widget?

Pass null to the onPressed property. Flutter renders the button in a disabled state automatically: reduced opacity, no tap response, screen reader announces disabled. Do not hide the button while it is disabled — the layout shift confuses users and breaks screen reader expectations.

How do I add an icon to a Flutter button?

Every M3 button class has a paired .icon constructor: FilledButton.icon, OutlinedButton.icon, ElevatedButton.icon, TextButton.icon. Pass an icon property (an Icon widget) and a label property (text). Reserve icons for buttons where the icon adds genuine recognition; avoid decorative icons that double the visual weight without informing the action.

How do I change the color of a Flutter button?

Use ButtonStyle through the styleFrom factory. FilledButton.styleFrom(backgroundColor: brand.primary, foregroundColor: brand.onPrimary). For state-dependent colors (hovered, pressed, focused), drop into ButtonStyle directly and use MaterialStateProperty.resolveWith. Set this at the theme level when possible so brand colors cascade across the app.

What is GFButton and when should I use it?

GFButton is part of the open-source GetWidget UI kit. It exposes shape variants (pills, square, social) that Material 3 does not ship natively, with a single type and shape API. Use it when your design system needs distinctly rounded, branded social, or specific button shapes that would require custom widgets otherwise. For standard M3 buttons in a typical app, the built-in FilledButton is the lower-overhead choice.

How do I make sure Flutter buttons are accessible?

Three rules. Enforce a 48 dp minimum tap target at the theme level. Use ColorScheme.fromSeed (which passes WCAG AA contrast for the standard tonal pairs) instead of hand-picked colors. Always pass a tooltip or Semantics label on icon-only buttons, because the screen reader cannot read an icon.

MORE IN /FLUTTER APP DEVELOPMENT COMPANY

Continue reading.

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
12m
flutter tabbar widget hero image
#flutter#tabbar

Flutter TabBar Widget in 2026: TabBar, TabBarView, TabController, and the Material 3 Primary vs Secondary Distinction

Build a Flutter TabBar widget to navigate between pages in a single view. Customize indicators, handle controllers, and pair with GetWidget's GFTabs.

Navin Sharma Navin Sharma
7m
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
Back to Blog