Skip to content

Updating the Material Buttons and their Themes #54776

Closed

Description

@HansMuller

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.

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'),
        ),
      ],
    );
  },
);

Screen Shot 2020-05-11 at 11 31 04 AM

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'),
        ),
      ],
    );
  },
);

Screen Shot 2020-05-11 at 11 33 19 AM

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

  1. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    p: material_uimaterial_ui package in flutter/packages
    c: proposalA detailed proposal for a change to Flutter
    on Apr 14, 2020
  2. bitsydarel commented on Apr 15, 2020

    @bitsydarel

    Any way to track the development process of this proposal?

  3. HansMuller commented on Apr 15, 2020

    @HansMuller
    ContributorAuthor

    @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.

  4. andbitty commented on Apr 18, 2020

    @andbitty

    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

  5. feinstein commented on Apr 20, 2020

    @feinstein
    Contributor

    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 Text already uses the style keyword and I never quite understood why, isn't it called theme).

  6. christian-muertz commented on Apr 20, 2020

    @christian-muertz
    Contributor

    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)

  7. feinstein commented on Apr 20, 2020

    @feinstein
    Contributor

    Yeah, precisely DefaultTextStyle is an exception to that rule, so I wonder, are those definitions officially documented somewhere? I don't remember seeing it.

  8. HansMuller commented on Apr 23, 2020

    @HansMuller
    ContributorAuthor

    @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 :-).

  9. HansMuller commented on Apr 23, 2020

    @HansMuller
    ContributorAuthor

    @superciccio Yes! Once the prototype is shared, I will put together an interactive DartPad example as well as some common-use-case/screenshots.

  10. HansMuller commented on Apr 24, 2020

    @HansMuller
    ContributorAuthor

    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.

  11. HansMuller commented on Apr 29, 2020

    @HansMuller
    ContributorAuthor

    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

    flutter_buttons:
      git: [email protected]:HansMuller/flutter_buttons.git

    in the dependencies section of your pubspec.yaml, and importing flutter_buttons.dart:

    import 'package:flutter_buttons/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 the lerp()` method, so the animations come for free. You'll have to try the demo to see the animations.

  12. feinstein commented on Apr 30, 2020

    @feinstein
    Contributor

    I see you are using theme nesting:

    TextButtonTheme(
          data: TextButtonThemeData(
            style: TextButton.styleFrom(primary: foregroundColor),
          ),
          child: ContainedButtonTheme(
            data: ContainedButtonThemeData(
              style: ContainedButton.styleFrom(
                primary: backgroundColor,
                onPrimary: foregroundColor,
              ),
            ),
            child: OutlinedButtonTheme(
              data: OutlinedButtonThemeData(
                style: TextButton.styleFrom(primary: foregroundColor),
              ),
              child: DefaultButtons(
                onPressed: () {
                  setState(() {
                    index = (index + 1) % foregroundColors.length;
                  });
                },
              ),
            ),
          ),
        );

    Maybe there could be a MultiTheme widget that could make this simpler, like MultiProvider does?

    MultiProvider(
      providers: [
        Provider<Something>(create: (_) => Something()),
        Provider<SomethingElse>(create: (_) => SomethingElse()),
        Provider<AnotherThing>(create: (_) => AnotherThing()),
      ],
      child: someWidget,
    )
  13. bitsydarel commented on Apr 30, 2020

    @bitsydarel

    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.

  14. HansMuller commented on Apr 30, 2020

    @HansMuller
    ContributorAuthor

    @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:

    MaterialApp(
      home: Home(),
      theme: ThemeData(
        textButtonTheme: TextButtonThemeData(
          style: TextButton.styleFrom(
            textStyle: headline4,
          ),
        ),
        containedButtonTheme: ContainedButtonThemeData(
          style: ContainedButton.styleFrom(
            textStyle: headline4,
          ),
        ),
        outlinedButtonTheme: OutlinedButtonThemeData(
          style: OutlinedButton.styleFrom(
            textStyle: headline4,
          ),
        ),
      ),
    )

    Or to just style a widget subtree:

    Theme(
      data: ThemeData(
        textButtonTheme: TextButtonThemeData(
          style: TextButton.styleFrom(
            textStyle: headline4,
          ),
        ),
        containedButtonTheme: ContainedButtonThemeData(
          style: ContainedButton.styleFrom(
            textStyle: headline4,
          ),
        ),
        outlinedButtonTheme: OutlinedButtonThemeData(
          style: OutlinedButton.styleFrom(
            textStyle: headline4,
          ),
        ),
      ),
      child: myWidgetSubtree,
    )

    I think that's what you're after.

  15. HansMuller commented on Apr 30, 2020

    @HansMuller
    ContributorAuthor

    @bitsydarel - this is a reasonable place to bring up #48195, looking now.

  16. feinstein commented on Apr 30, 2020

    @feinstein
    Contributor

    @HansMuller yes it makes sense, I forgot about it.

  17. HansMuller commented on Apr 30, 2020

    @HansMuller
    ContributorAuthor

    @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.

  18. feinstein commented on Apr 30, 2020

    @feinstein
    Contributor

    If it's a problem on AppBar and themes, than I think it might be related to my issue #50606 so I am going to just leave it here as a reference.

  19. bitsydarel commented on Apr 30, 2020

    @bitsydarel

    @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.

  20. HansMuller commented on Apr 30, 2020

    @HansMuller
    ContributorAuthor

    @feinstein - Thanks for leaving the reference to #50606. We'll tackle AppBar once buttons and their themes are sorted.

  21. HansMuller commented on Apr 30, 2020

    @HansMuller
    ContributorAuthor

    @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.

  22. HansMuller commented on May 2, 2020

    @HansMuller
    ContributorAuthor

    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:

    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(),
          ),
        ),
      ),
    )

  23. jpc-ae commented on Jun 1, 2020

    @jpc-ae

    I just noticed that the icons don't seem to follow the text size animation. It's a bit jarring.

  24. github-actions commented on Aug 19, 2021

    @github-actions

    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 -v and a minimal reproduction of the issue.

  25. locked as resolved and limited conversation to collaborators on Aug 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: proposalA detailed proposal for a change to Flutterframeworkflutter/packages/flutter repository. See also f: labels.p: material_uimaterial_ui package in flutter/packages

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions