Repository navigation
Updating the Material Buttons and their Themes #54776
Description
To address the many problems that have been reported about the Material Theme system and, in particular, the button classes and their theme, we've made some long-term plans for improving things.
- Material Theme System Updates (flutter.dev/go/material-theme-system-updates) outlines the overall problem and provides detail in some areas.
- Updating the Material Buttons and their Themes (flutter.dev/go/material-button-system-updates) is a relatively detailed proposal for how to remedy the problems with buttons and their themes.
This issue is the Flutter GitHub connection to flutter.dev/go/material-button-system-updates proposal. I'll summarize how the proposal addresses problems that have been raised in other issues here. If you have feedback about the proposal you're welcome to add marginal comments the proposal document, or you can leave comments here. If you have you a lot to say, or if you'd like to incorporate code or screenshots into your comments, you may find that leaving your feedback here is more convenient.
Demo
If you'd like to skip the explanation and jump write to the visuals or the source code,
there's a DartPad demo of the prototype. The source code is in the DartPad demo of course (and all in one file), and the Flutter framework part is hosted here: https://github.com/HansMuller/flutter_buttons.
Summary: Updating the Material Buttons and their Themes
Rather than try and evolve the existing button classes and their theme in-place, the proposal introduces new replacement button widgets and themes. In addition to freeing us from the backwards compatibility labyrinthe that evolving the existing classes in-place would entail, the new names sync Flutter back up with the Material Design spec, which uses the new names for the button components.
| Old Widget | Old Theme | New Widget | New Theme |
|---|---|---|---|
| FlatButton | ButtonTheme | TextButton | TextButtonTheme |
| RaisedButton | ButtonTheme | ContainedButton | ContainedButtonTheme |
| OutlineButton | ButtonTheme | OutlinedButton | OutlinedButtonTheme |
The new themes follow the "normalized" pattern that Flutter adopted for new Material widgets about a year ago. Theme properties are null by default, and although ThemeData has a slot for each button theme, they're also null by default. Implementing and documenting default values are the sole responsibility of the button components. Themes can override the component defaults. If no override is specified the component defers to the overall Theme's colorScheme and textTheme.
ButtonStyle
We've added a new class called ButtonStyle which aggregates the buttons' visual properties. Most of ButtonStyle's properties are defined with MaterialStateProperty, so that they can represent different values for different button states.
Each of the new button widget classes has a static styleFrom() method that returns a ButtonStyle given one or more ColorScheme colors. The styleFrom() method computes all of the dependent colors for all of the button's states.
Using the Theme to override a Button property like textColor
This issue was raised in #19623
An app that uses TextButton and ContainedButton (nee FlatButton and RaisedButton) can configure the text color for all buttons by specifying a textButtonTheme and containedButtonTheme in the app's overall theme.
MaterialApp(
theme: ThemeData(
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(primary: Colors.green)
),
containedButtonTheme: ContainedButtonThemeData(
style: ContainedButton.styleFrom(onPrimary: Colors.green)
),
),
// ...
)The TextButton's text is rendered in the ColorScheme's primary color by default, and the ContainedButton's text is rendered with the onPrimary color. We've created a new ButtonStyle for each of the corresponding themes that effectively overrides the text color. ContainedButtons use the primary color as background, so one would probably want to set that as well:
MaterialApp(
theme: ThemeData(
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(primary: Colors.green)
),
containedButtonTheme: ContainedButtonThemeData(
style: ContainedButton.styleFrom(
primary: Colors.yellow,
onPrimary: Colors.green,
)
),
),
// ...
)In both cases this approach creates a ButtonStyle where the other colors that depend on the primary or onPrimary colors have been updated as well. For example the highlight color that's shown when the button is tapped or focused is also included in the ButtonStyle because it depends on the primary color too.
This is usually what you want. However there are times when an app needs to more precisely control its buttons' appearance. For example, you might really want to only change the ContainedButton's text color, and for all possible states (focused, pressed, disabled, etc). To do that, create a ButtonStyle that specifies textColor:
MaterialApp(
theme: ThemeData(
containedButtonTheme: ContainedButtonThemeData(
style: ButtonStyle(
textColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) => Colors.green,
),
),
),
),
// ...
)The ButtonStyle's textColor is a MaterialProperty<Color> and in this case the property just maps all possible states to green. To only override the button's text color when the button was enabled:
MaterialApp(
theme: ThemeData(
containedButtonTheme: ContainedButtonThemeData(
style: ButtonStyle(
textColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return (states.contains(MaterialState.disabled)) ? null : Colors.green;
},
),
),
),
),
// ...
)The MaterialStateProperty that we've created for the text color returns null when its button is disabled. That means that the component will use the default: either the widget's ButtonStyle parameter, or, if that's null too, then the widget's internal default.
Using the Theme to override Button shapes
ButtonStyle objects allow one to override all of the visual properties including the buttons' shapes. As noted below, one can give all of an app's buttons the "stadium" shape like this.
MaterialApp(
home: Home(),
theme: ThemeData(
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: StadiumBorder(),
),
),
containedButtonTheme: ContainedButtonThemeData(
style: ContainedButton.styleFrom(
shape: StadiumBorder(),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
),
),
),
)Using ButtonStyle to change the appearance of individual buttons
A ButtonStyle can also be applied to individual buttons. For example, to create an AlertDialog with "stadium" shaped action buttons, rather than wrapping the dialog's contents in a theme, one could just specify the same style for both buttons.
ButtonStyle style = OutlinedButton.styleFrom(shape: StadiumBorder());
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('AlertDialog Title'),
content: Text('Stadium shaped action buttons, default outline'),
actions: <Widget>[
OutlinedButton(
style: style,
onPressed: () { dismissDialog(); },
child: Text('Approve'),
),
OutlinedButton(
style: style,
onPressed: () { dismissDialog(); },
child: Text('Really Approve'),
),
],
);
},
);In this case, just like the others, the style only overrides the button shapes, all of of the other properties get their context-specific defaults in the usual way. To give one of the buttons a heavier primary colored outline, instead of the default thin gray outline:
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('AlertDialog Title'),
content: Text('One Stadium shaped action button, with a heavy, primary color outline.',
actions: <Widget>[
OutlinedButton(
style: OutlinedButton.styleFrom(shape: StadiumBorder()),
onPressed: () { dismissDialog(); },
child: Text('Approve'),
),
OutlinedButton(
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
side: BorderSide(
width: 2,
color: Theme.of(context).colorScheme.primary,
),
),
onPressed: () { dismissDialog(); },
child: Text('Really Approve'),
),
],
);
},
);Most of the button visual properties are specified in terms of MaterialStateProperty, which means that the property can have different values depending on its button's state. Using the convenient static styleFrom methods delegates creating the MaterialStateProperty values to the button class. It's easy enough to create them directly, to construct ButtonStyles with state-specific values. For example, to set up the second dialog buttons so that it only shows the heavier primary colored outline when it's hovered or focused:
OutlinedButton(
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
).copyWith(
side: MaterialStateProperty.resolveWith<BorderSide>((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered) || states.contains(MaterialState.focused)) {
return BorderSide(
width: 2,
color: Theme.of(context).colorScheme.primary,
);
}
return null; // defer to the default
},
)),
onPressed: () { dismissDialog(); },
child: Text('Really Approve'),
),In this case we've used the resolveWith() utility method to create a MaterialStateProperty that only overrides the default outline appearance when the button is either focused or hovered.



Activity
bitsydarel commentedon Apr 15, 2020
Any way to track the development process of this proposal?
HansMuller commentedon Apr 15, 2020
@bitsydarel Feedback will hopefully appear here or in flutter.dev/go/material-button-system-updates and I'll provide a pointer to a prototype here soon.
andbitty commentedon Apr 18, 2020
can the flutter team, create as much image as possible with some combinations of these styles and proposal. like an image of what is being rendered when a style is applied.
thanks
feinstein commentedon Apr 20, 2020
Is it there a reason to use the term "Style", instead of "Theme"? Are "Styles" different of "Themes"? I might be pedantic here, but usually having two terms that lead to the same thing is confusing and misleading, also is one less word to be used if we need a different behavior for something similar. (Although I recognize
Textalready uses thestylekeyword and I never quite understood why, isn't it calledtheme).christian-muertz commentedon Apr 20, 2020
I think styles are passed directly to widgets to style them. Themes are used to theme multiple parts of your application which basically means creating default looks for the subtree. Styles always override stuff coming from the next theme. Themes are more general and styles are more specific. (I am currently confused with DefaultTextStyle. Maybe because TextTheme is about defining different TextStyles for different types of text)
feinstein commentedon Apr 20, 2020
Yeah, precisely
DefaultTextStyleis an exception to that rule, so I wonder, are those definitions officially documented somewhere? I don't remember seeing it.HansMuller commentedon Apr 23, 2020
@christian-muertz - yes.
@feinstein The analog for ButtonStyle is TextStyle rather than ButtonTheme. In the Material library, FooTheme's are inherited widgets configured with FooThemeData values per the "Normalized Component Theme" pattern outlined in https://flutter.dev/go/material-theme-system-updates. ButtonStyle is just a bundle of properties that most buttons have mostly in common.
It's true that the widget library includes a widget called "DefaultTextStyle" that behaves like a theme, and another called IconTheme. The Material library includes something else again called TextTheme and and an enum that I wish didn't exist at all called ButtonTextTheme. There really isn't a pattern in any of that, and we're definitely not following it :-).
HansMuller commentedon Apr 23, 2020
@superciccio Yes! Once the prototype is shared, I will put together an interactive DartPad example as well as some common-use-case/screenshots.
HansMuller commentedon Apr 24, 2020
There's a prototype of the new button themes and classes here:
https://github.com/HansMuller/flutter_buttons. I'll continue to update this package until it's ready to be integrated with the flutter repo.
If you're curious, you can try it out now per the trivial example in the README. Documentation, tests, and proper examples to come.
HansMuller commentedon Apr 29, 2020
New Buttons and Themes Demo App
I've made a few updates to the prototype implementation to correct some details. I've also written an app - deployed in DartPad - that shows the new buttons draped in the default purple Material theme. It also demonstrates configuring the text color for all buttons, as well as configuring the text style for all buttons, or just one. I will extend the demo to include more common button theme scenarios soon.
The demo runs in DartPad: https://dartpad.dev/5de3692f02bf805c91f7deb40429b573
To simplifying running the code in DartPad I just put all of the code in one file.
If you like, you can try using the new buttons and themes in your own test app and by including
in the
dependenciessection of your pubspec.yaml, and importing flutter_buttons.dart:Here's a screenshot of the demo on an Android phone. The first page shows the default configuration of the three button types: TextButton, ContainedButton, OutlinedButton. Pressing the star button in the AppBar switches to the dark mode.
The second page demonstrates overriding the buttons' text color. Pressing any button changes the color. The demo also changes the contained button's primary color, which is used to fill the button's background.
The last two demos use the button themes to override the buttons' text styles. The theme's and their
ButtonStyle's support thelerp()` method, so the animations come for free. You'll have to try the demo to see the animations.feinstein commentedon Apr 30, 2020
I see you are using theme nesting:
Maybe there could be a
MultiThemewidget that could make this simpler, likeMultiProviderdoes?bitsydarel commentedon Apr 30, 2020
Hi, since we’re talking about theme, I faced this issue again #48195 few days ago while doing a demo of a POC. Is there any plan to fix that or update the docs ?
@HansMuller Now that I think of it, here might not be the best place to leave this comment, but I really want to get your attention on it. I believe themes should be fully customisable, even in dark mode. So no value should be forced upon the developers.
HansMuller commentedon Apr 30, 2020
@feinstein - I nested the the button themes in the demo because the buttons prototype can't rely on support from the Material Theme. I'd covered this issue in the flutter_button repos README however I forgot to mention it here.
When the classes in this repo are part of the Flutter framework, the button themes would be created as part of the app's overall theme. Instead of nesting the three button themes, one could write:
Or to just style a widget subtree:
I think that's what you're after.
HansMuller commentedon Apr 30, 2020
@bitsydarel - this is a reasonable place to bring up #48195, looking now.
feinstein commentedon Apr 30, 2020
@HansMuller yes it makes sense, I forgot about it.
HansMuller commentedon Apr 30, 2020
@bitsydarel - AppBar has some of the same historical issues that the button classes had, although the situation isn't quite as dire :-).
AppBar should configure its default background color to
Theme.of(context).colorScheme.primary' and the color for text and icons to...colorScheme.onPrimary`. That works for dark and light and custom color schemes.We will get there, although I've targeted the buttons and their themes first because their usability (in terms of theming) is the lowest.
feinstein commentedon Apr 30, 2020
If it's a problem on
AppBarand themes, than I think it might be related to my issue #50606 so I am going to just leave it here as a reference.bitsydarel commentedon Apr 30, 2020
@HansMuller that’s correct. Many widgets depend on ThemeData.primary* rather than colorScheme.* .
What’s important is that changes are being made to address that. The only issue is that you have track down which property of ThemeData the widget is secretly using and then create workarounds.
By the way I hope the new buttons use the colorScheme properties if there’s no specific buttontheme* specified.
HansMuller commentedon Apr 30, 2020
@feinstein - Thanks for leaving the reference to #50606. We'll tackle AppBar once buttons and their themes are sorted.
HansMuller commentedon Apr 30, 2020
@bitsydarel - YES. The default values for the new buttons are defined exclusively by the button classes, and they only depend on the Theme's colorScheme and textTheme.
HansMuller commentedon May 2, 2020
There's an updated version of the demo that shows how one can theme the buttons' shapes.
As noted earlier, the current implementation nests the button component themes. In the final version one will be able to configure a Theme directly. To give all of an app's buttons the "stadium" shape:
jpc-ae commentedon Jun 1, 2020
I just noticed that the icons don't seem to follow the text size animation. It's a bit jarring.
github-actions commentedon Aug 19, 2021
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.