Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow arbitrary foreground and background elements for ButtonStyle #139456

Closed
2 tasks done
caseycrogers opened this issue Dec 4, 2023 · 7 comments · Fixed by #141818 or #142748
Closed
2 tasks done

Allow arbitrary foreground and background elements for ButtonStyle #139456

caseycrogers opened this issue Dec 4, 2023 · 7 comments · Fixed by #141818 or #142748
Assignees
Labels
c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. P2 Important issues not at the top of the work list r: fixed Issue is closed as already fixed in a newer version team-design Owned by Design Languages team triaged-design Triaged by Design Languages team

Comments

@caseycrogers
Copy link
Contributor

caseycrogers commented Dec 4, 2023

Is there an existing issue for this?

Use case

I want to add fairly arbitrary visual details to my app's ElevatedButton's (this feature request applies to all the material button types, but I'll use ElevatedButton as a stand in).

Here are some examples:

  1. Adding a linear/radial/etc gradient
  2. Adding a border
  3. Using a semi-opaque image to texture the button
  4. Using an arbitrary box shadow instead of the material elevation system
  5. Placing .svgs above, behind and around the button as visual accents

As-is, ButtonStyle limits me to a very narrow range of style customization: shape, color, elevation...

I could create my own button system, but the material buttons do an enormous amount of extremely helpful things (manage splash factory, handle material state, tune visual density to match the platform, etc) that would be a huge hassle to reverse engineer.

Here are some examples of random apps in the wild that have buttons that are not achievable without heavy workarounds.

My app. I use a linear gradient and a border using a complicated work around that puts every elevated button in a stack:

Marvel snap. Marvel snap uses a particularly complex system containing tons of elements:

  1. asymmetric shape
  2. Texture
  3. Texture fades to solid background in center
  4. Stroke text
  5. Metallic border
  6. Light flare
  7. And more...

Proposal

I think there are a lot of different ways to add greater customization to ButtonStyle. I will list a few I've thought of here, I'm very open to more suggestions!

  1. Mimic Container's foreground and background BoxDecoration arguments
    I don't love this solution as it's fairly limiting. For example, you can't use a Stack to stack elements on top of each other in this context.
  2. Include foreground and background builder arguments. The builders would take in a context, the current material state(s) and the EdgeInsets by which the button is padded with (as they should be able to draw in and outside of the button without having to reverse engineer the complicated padding logic).
    I like this much more, as it gives me completely arbitrary design control. For example, even Marvel Snap's insanely complicated button would probably be doable with this system. The implementation would need to ensure the foreground child doesn't interfere with hit testing. The disadvantage is that even pretty simple customizations (like adding a linear gradient) would be pretty complicated.

In both cases, the background argument would be placed between the button's child and the background Material. And/or the Material could be left out entirely if the background argument is non null.

Maybe to ensure that simple customization is easy while complex customization is still possible, BOTH BoxDecoration and builder based customization could be provided?

What this might look like to use (coded off the top of my head, probably has minor syntax errors):

ElevatedButton(
  style: ButtonStyle(
    backgroundBuilder: (context, padding, materialStates, style) {
       final baseColor = style.resolveFor(materialStates);
       final shape = style.shape;
       return Padding(
         // Pad the background by the inherited padding.
         // Elements could be placed above this padding to draw outside
         // of the elevated button's background.
         padding: padding,
         child: _ClipToShape(
           shape: shape, 
           child: Stack(
             children: [
               _MyGradient(color:  baseColor),
               _MySvgAccent(),
               // And so on.
             ],
           )
         ),
       );
    },
  ),
);
@danagbemava-nc danagbemava-nc added in triage Presently being triaged by the triage team c: new feature Nothing broken; request for a new capability framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. c: proposal A detailed proposal for a change to Flutter team-design Owned by Design Languages team and removed in triage Presently being triaged by the triage team labels Dec 4, 2023
@HansMuller HansMuller added the P3 Issues that are less important to the Flutter project label Dec 6, 2023
@HansMuller
Copy link
Contributor

Good suggestion here and, as you've pointed out, getting the design right means sorting out how to make common cases - like a gradient background - easy, and the tricky cases possible. Leaving the Material background in place is probably important, since it deals with the button's shape. Providing a way to stack a background widget on top of the Material (and clipped to the Material) does seem like a good general purpose idea.

If you're interested in pursuing this further, prototyping some common cases and looking for ways to simplify writing them would be a helpful to continue.

@HansMuller HansMuller added the triaged-design Triaged by Design Languages team label Dec 6, 2023
@caseycrogers
Copy link
Contributor Author

Here's my hacky way of creating an arbitrarily customizable elevated button, if that helps for inspiration:
6-daily_won

        return Stack(
          children: [
            Positioned.fill(
              child: Container(
                margin: const EdgeInsets.symmetric(vertical: 4),
                foregroundDecoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(context.gutterTiny),
                  border: Border.all(
                    width: context.gutterTiny,
                    color: Theme.of(context).colorScheme.shadow,
                  ),
                ),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(context.gutterTiny),
                  gradient: kButtonGradient(context, backgroundColor),
                ),
              ),
            ),
            ElevatedButtonTheme(
              data: ElevatedButtonThemeData(
                style: effectiveStyle.copyWith(
                  backgroundColor:
                      const MaterialStatePropertyAll(Colors.transparent),
                  foregroundColor: MaterialStatePropertyAll(
                    foregroundColor,
                  ),
                ),
              ),
              child: ElevatedButton(
                onPressed: onTap != null ? () => wrap(onTap) : onTap,
                child: ShaderMask(
                  shaderCallback: (bounds) {
                    return LinearGradient(
                      begin: const Alignment(0.005, 1),
                      end: const Alignment(-0.005, -1),
                      stops: const [.2, .55],
                      colors: [
                        Theme.of(context).shadowColor.withOpacity(.1),
                        Theme.of(context).shadowColor.withOpacity(0),
                      ],
                    ).createShader(bounds);
                  },
                  blendMode: BlendMode.srcATop,
                  child: child,
                ),
              ),
            ),
          ],
        );

@HansMuller
Copy link
Contributor

HansMuller commented Jan 3, 2024

I played around with the ShaderMask part of your example a little (DartPad).

Screenshot 2024-01-03 at 3 10 42 PM

The result is still a little complicated:

ElevatedButton(
  onPressed: () { },
  style: ElevatedButton.styleFrom(
    textStyle: theme.textTheme.displayLarge,
  ).copyWith(
    // Lighten the hovered overlay color a little so that it doesn't
    // obscure the faded part of the shader masked child.
    overlayColor: MaterialStateProperty.resolveWith(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.hovered)) {
          return colorScheme.primary.withOpacity(0.04);
        }
        return null; // defer to the default
      },
    ),
  ),
  child: ShaderMask(
    shaderCallback: (Rect bounds) {
      return LinearGradient(
        begin: Alignment.bottomCenter,
        end: Alignment.topCenter,
        colors: <Color>[
          colorScheme.primary,
          colorScheme.primaryContainer,
        ],
      ).createShader(bounds);
    },
    blendMode: BlendMode.srcATop,
    child: const Text('Elevated Button'),
  ),
)

Doing so highlighted the fact it's not really practical for the child to respond to state changes in the same way that we'd like to have a background widget (stacked behind the child) also respond to state changes. As well as being informed of the button's shape and the child's insets. I have a vague idea about how to address these limitations without breaking backwards compatibility. The vague idea is that the background (currently the a Material widget) and the foreground (currently just the child) would be constructed by callbacks along the lines of what you've proposed earlier. Will try and make a concrete proposal here soon.

@HansMuller
Copy link
Contributor

HansMuller commented Jan 3, 2024

This issue is a generalization of #130335 and #89563.

@HansMuller
Copy link
Contributor

HansMuller commented Jan 3, 2024

Giving a button a gradient background isn't too difficult. Here's an example (DartPad) based on one provided by @TahaTesser in #130335. It requires one to restore the button's padding, by padding the button's child, and the configuration requires turning on clipping and using an Ink widget. As you can see by comparing this example with the previous one, my quick guess about the padding wasn't quite right.

Screenshot 2024-01-03 at 3 10 42 PM

This isn't too complicated:

ElevatedButton(
  onPressed: () {},
  clipBehavior: Clip.antiAlias, // Buttons don't clip their child by default.
  style: ElevatedButton.styleFrom(
    padding: EdgeInsets.zero,  // So that the gradient fills the entire button.
    textStyle: theme.textTheme.displayLarge,
  ),
  child: Ink( // So that the button's overlay continues to appear.
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: <Color>[
          colorScheme.primaryContainer,
          colorScheme.tertiaryContainer,
        ],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
    child: const Padding(
      padding: EdgeInsets.symmetric(horizontal: 16),
      child: Text('Elevated Button'),
    ),
  ),
)

Because the gradient background is introduced as part of the child, it's not possible to configure the ElevatedButtonTheme to give all elevated buttons a gradient background (this is also true for the previous example). And, as before, this example doesn't address creating a background that changes based on the button's state.

@HansMuller
Copy link
Contributor

I've addressed most of the issues described here in #141818. There's a detailed explanation of the new API in the PR's description.

@HansMuller HansMuller added the P2 Important issues not at the top of the work list label Jan 19, 2024
@HansMuller HansMuller self-assigned this Jan 19, 2024
@HansMuller HansMuller removed the P3 Issues that are less important to the Flutter project label Jan 19, 2024
auto-submit bot pushed a commit that referenced this issue Feb 1, 2024
…#141818)

Fixes #139456, #130335, #89563.

Two new properties have been added to ButtonStyle to make it possible to insert arbitrary state-dependent widgets in a button's background or foreground. These properties can be specified for an individual button, using the style parameter, or for all buttons using a button theme's style parameter.

The new ButtonStyle properties are `backgroundBuilder` and `foregroundBuilder` and their (function) types are:

```dart
typedef ButtonLayerBuilder = Widget Function(
  BuildContext context,
  Set<MaterialState> states,
  Widget? child
);
```

The new builder functions are called whenever the button is built and the `states` parameter communicates the pressed/hovered/etc state fo the button.

## `backgroundBuilder`

Creates a widget that becomes the child of the button's Material and whose child is the rest of the button, including the button's `child` parameter.  By default the returned widget is clipped to the Material's ButtonStyle.shape.

The `backgroundBuilder` can be used to add a gradient to the button's background. Here's an example that creates a yellow/orange gradient background:

![opaque-gradient-bg](https://github.com/flutter/flutter/assets/1377460/80df8368-e7cf-49ef-aee7-2776a573644c)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Because the background widget becomes the child of the button's Material, if it's opaque (as it is in this case) then it obscures the overlay highlights which are painted on the button's Material. To ensure that the highlights show through one can decorate the background with an `Ink` widget.  This version also overrides the overlay color to be (shades of) red, because that makes the highlights look a little nicer with the yellow/orange background.

![ink-gradient-bg](https://github.com/flutter/flutter/assets/1377460/68a49733-f30e-44a1-a948-dc8cc95e1716)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Now the button's overlay highlights are painted on the Ink widget. An Ink widget isn't needed if the background is sufficiently translucent. This version of the example creates a translucent backround widget. 

![translucent-graident-bg](https://github.com/flutter/flutter/assets/1377460/3b016e1f-200a-4d07-8111-e20d29f18014)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [
            Colors.orange.withOpacity(0.5),
            Colors.yellow.withOpacity(0.5),
          ]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

One can also decorate the background with an image. In this example, the button's background is an burlap texture image. The foreground color has been changed to black to make the button's text a little clearer relative to the mottled brown backround.

![burlap-bg](https://github.com/flutter/flutter/assets/1377460/f2f61ab1-10d9-43a4-bd63-beecdce33b45)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.black,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(burlapUrl),
            fit: BoxFit.cover,
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The background widget can depend on the `states` parameter. In this example the blue/orange gradient flips horizontally when the button is hovered/pressed.

![gradient-flip](https://github.com/flutter/flutter/assets/1377460/c6c6fe26-ae47-445b-b82d-4605d9583bd8)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The preceeding examples have not included a BoxDecoration border because ButtonStyle already supports `ButtonStyle.shape` and `ButtonStyle.side` parameters that can be uesd to define state-dependent borders. Borders defined with the ButtonStyle side parameter match the button's shape. To add a border that changes color when the button is hovered or pressed, one must specify the side property using `copyWith`, since there's no `styleFrom` shorthand for this case.

![border-gradient-bg](https://github.com/flutter/flutter/assets/1377460/63cffcd3-0dcf-4eb1-aed5-d14adf1e57f6)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.indigo,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ).copyWith(
    side: MaterialStateProperty.resolveWith<BorderSide?>((Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return BorderSide(width: 3, color: Colors.yellow);
      }
      return null; // defer to the default
    }),
  ),
  child: Text('Text Button'),
)
```

Although all of the examples have created a ButtonStyle locally and only applied it to one button, they could have configured the `ThemeData.textButtonTheme` instead and applied the style to all TextButtons. And, of course, all of this works for all of the ButtonStyleButton classes, not just TextButton.

## `foregroundBuilder`

Creates a Widget that contains the button's child parameter. The returned widget is clipped by the button's [ButtonStyle.shape] inset by the button's [ButtonStyle.padding] and aligned by the button's [ButtonStyle.alignment].

The `foregroundBuilder` can be used to wrap the button's child, e.g. with a border or a `ShaderMask` or as a state-dependent substitute for the child.

This example adds a border that's just applied to the child. The border only appears when the button is hovered/pressed.

![border-fg](https://github.com/flutter/flutter/assets/1377460/687a3245-fe68-4983-a04e-5fcc77f8aa21)

```dart
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return DecoratedBox(
        decoration: BoxDecoration(
          border: states.contains(MaterialState.hovered)
            ? Border(bottom: BorderSide(color: colorScheme.primary))
            : Border(), // essentially "no border"
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The foregroundBuilder can be used with `ShaderMask` to change the way the button's child is rendered. In this example the ShaderMask's gradient causes the button's child to fade out on top.

![shader_mask_fg](https://github.com/flutter/flutter/assets/1377460/54010f24-e65d-4551-ae58-712135df3d8d)

```dart
ElevatedButton(
  onPressed: () { },
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return ShaderMask(
        shaderCallback: (Rect bounds) {
          return LinearGradient(
            begin: Alignment.bottomCenter,
            end: Alignment.topCenter,
            colors: <Color>[
              colorScheme.primary,
              colorScheme.primaryContainer,
            ],
          ).createShader(bounds);
        },
        blendMode: BlendMode.srcATop,
        child: child,
      );
    },
  ),
  child:  const Text('Elevated Button'),
)
```

A commonly requested configuration for butttons has the developer provide images, one for pressed/hovered/normal state. You can use the foregroundBuilder to create a button that fades between a normal image and another image when the button is pressed. In this case the foregroundBuilder doesn't use the child it's passed, even though we've provided the required TextButton child parameter.

![image-button](https://github.com/flutter/flutter/assets/1377460/f5b1a22f-43ce-4be3-8e70-06de4c958380)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final String url = states.contains(MaterialState.pressed) ? smiley2Url : smiley1Url;
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```

In this example the button's default overlay appears when the button is hovered and pressed. Another image can be used to indicate the hovered state and the default overlay can be defeated by specifying `Colors.transparent` for the `overlayColor`:

![image-per-state](https://github.com/flutter/flutter/assets/1377460/7ab9da2f-f661-4374-b395-c2e0c7c4cf13)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.transparent,
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      String url = states.contains(MaterialState.hovered) ? smiley3Url : smiley1Url;
      if (states.contains(MaterialState.pressed)) {
        url = smiley2Url;
      }
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```
auto-submit bot added a commit that referenced this issue Feb 1, 2024
…ndBuilder" (#142748)

Reverts #141818
Initiated by: XilaiZhang
This change reverts the following previous change:
Original Description:
Fixes #139456, #130335, #89563.

Two new properties have been added to ButtonStyle to make it possible to insert arbitrary state-dependent widgets in a button's background or foreground. These properties can be specified for an individual button, using the style parameter, or for all buttons using a button theme's style parameter.

The new ButtonStyle properties are `backgroundBuilder` and `foregroundBuilder` and their (function) types are:

```dart
typedef ButtonLayerBuilder = Widget Function(
  BuildContext context,
  Set<MaterialState> states,
  Widget? child
);
```

The new builder functions are called whenever the button is built and the `states` parameter communicates the pressed/hovered/etc state fo the button.

## `backgroundBuilder`

Creates a widget that becomes the child of the button's Material and whose child is the rest of the button, including the button's `child` parameter.  By default the returned widget is clipped to the Material's ButtonStyle.shape.

The `backgroundBuilder` can be used to add a gradient to the button's background. Here's an example that creates a yellow/orange gradient background:

![opaque-gradient-bg](https://github.com/flutter/flutter/assets/1377460/80df8368-e7cf-49ef-aee7-2776a573644c)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Because the background widget becomes the child of the button's Material, if it's opaque (as it is in this case) then it obscures the overlay highlights which are painted on the button's Material. To ensure that the highlights show through one can decorate the background with an `Ink` widget.  This version also overrides the overlay color to be (shades of) red, because that makes the highlights look a little nicer with the yellow/orange background.

![ink-gradient-bg](https://github.com/flutter/flutter/assets/1377460/68a49733-f30e-44a1-a948-dc8cc95e1716)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [Colors.orange, Colors.yellow]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

Now the button's overlay highlights are painted on the Ink widget. An Ink widget isn't needed if the background is sufficiently translucent. This version of the example creates a translucent backround widget. 

![translucent-graident-bg](https://github.com/flutter/flutter/assets/1377460/3b016e1f-200a-4d07-8111-e20d29f18014)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.red,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(colors: [
            Colors.orange.withOpacity(0.5),
            Colors.yellow.withOpacity(0.5),
          ]),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

One can also decorate the background with an image. In this example, the button's background is an burlap texture image. The foreground color has been changed to black to make the button's text a little clearer relative to the mottled brown backround.

![burlap-bg](https://github.com/flutter/flutter/assets/1377460/f2f61ab1-10d9-43a4-bd63-beecdce33b45)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.black,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      return Ink(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(burlapUrl),
            fit: BoxFit.cover,
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The background widget can depend on the `states` parameter. In this example the blue/orange gradient flips horizontally when the button is hovered/pressed.

![gradient-flip](https://github.com/flutter/flutter/assets/1377460/c6c6fe26-ae47-445b-b82d-4605d9583bd8)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The preceeding examples have not included a BoxDecoration border because ButtonStyle already supports `ButtonStyle.shape` and `ButtonStyle.side` parameters that can be uesd to define state-dependent borders. Borders defined with the ButtonStyle side parameter match the button's shape. To add a border that changes color when the button is hovered or pressed, one must specify the side property using `copyWith`, since there's no `styleFrom` shorthand for this case.

![border-gradient-bg](https://github.com/flutter/flutter/assets/1377460/63cffcd3-0dcf-4eb1-aed5-d14adf1e57f6)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.indigo,
    backgroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final Color color1 = Colors.blue.withOpacity(0.5);
      final Color color2 = Colors.orange.withOpacity(0.5);
      return DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: switch (states.contains(MaterialState.hovered)) {
              true => <Color>[color1, color2],
              false => <Color>[color2, color1],
            },
          ),
        ),
        child: child,
      );
    },
  ).copyWith(
    side: MaterialStateProperty.resolveWith<BorderSide?>((Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return BorderSide(width: 3, color: Colors.yellow);
      }
      return null; // defer to the default
    }),
  ),
  child: Text('Text Button'),
)
```

Although all of the examples have created a ButtonStyle locally and only applied it to one button, they could have configured the `ThemeData.textButtonTheme` instead and applied the style to all TextButtons. And, of course, all of this works for all of the ButtonStyleButton classes, not just TextButton.

## `foregroundBuilder`

Creates a Widget that contains the button's child parameter. The returned widget is clipped by the button's [ButtonStyle.shape] inset by the button's [ButtonStyle.padding] and aligned by the button's [ButtonStyle.alignment].

The `foregroundBuilder` can be used to wrap the button's child, e.g. with a border or a `ShaderMask` or as a state-dependent substitute for the child.

This example adds a border that's just applied to the child. The border only appears when the button is hovered/pressed.

![border-fg](https://github.com/flutter/flutter/assets/1377460/687a3245-fe68-4983-a04e-5fcc77f8aa21)

```dart
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return DecoratedBox(
        decoration: BoxDecoration(
          border: states.contains(MaterialState.hovered)
            ? Border(bottom: BorderSide(color: colorScheme.primary))
            : Border(), // essentially "no border"
        ),
        child: child,
      );
    },
  ),
  child: Text('Text Button'),
)
```

The foregroundBuilder can be used with `ShaderMask` to change the way the button's child is rendered. In this example the ShaderMask's gradient causes the button's child to fade out on top.

![shader_mask_fg](https://github.com/flutter/flutter/assets/1377460/54010f24-e65d-4551-ae58-712135df3d8d)

```dart
ElevatedButton(
  onPressed: () { },
  style: ElevatedButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final ColorScheme colorScheme = Theme.of(context).colorScheme;
      return ShaderMask(
        shaderCallback: (Rect bounds) {
          return LinearGradient(
            begin: Alignment.bottomCenter,
            end: Alignment.topCenter,
            colors: <Color>[
              colorScheme.primary,
              colorScheme.primaryContainer,
            ],
          ).createShader(bounds);
        },
        blendMode: BlendMode.srcATop,
        child: child,
      );
    },
  ),
  child:  const Text('Elevated Button'),
)
```

A commonly requested configuration for butttons has the developer provide images, one for pressed/hovered/normal state. You can use the foregroundBuilder to create a button that fades between a normal image and another image when the button is pressed. In this case the foregroundBuilder doesn't use the child it's passed, even though we've provided the required TextButton child parameter.

![image-button](https://github.com/flutter/flutter/assets/1377460/f5b1a22f-43ce-4be3-8e70-06de4c958380)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      final String url = states.contains(MaterialState.pressed) ? smiley2Url : smiley1Url;
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```

In this example the button's default overlay appears when the button is hovered and pressed. Another image can be used to indicate the hovered state and the default overlay can be defeated by specifying `Colors.transparent` for the `overlayColor`:

![image-per-state](https://github.com/flutter/flutter/assets/1377460/7ab9da2f-f661-4374-b395-c2e0c7c4cf13)

```dart
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    overlayColor: Colors.transparent,
    foregroundBuilder: (BuildContext context, Set<MaterialState> states, Widget? child) {
      String url = states.contains(MaterialState.hovered) ? smiley3Url : smiley1Url;
      if (states.contains(MaterialState.pressed)) {
        url = smiley2Url;
      }
      return AnimatedContainer(
        width: 100,
        height: 100,
        duration: Duration(milliseconds: 300),
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(url),
            fit: BoxFit.contain,
          ),
        ),
      );
    },
  ),
  child: Text('No Child'),
)
```
@danagbemava-nc danagbemava-nc added the r: fixed Issue is closed as already fixed in a newer version label Feb 5, 2024
Copy link

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.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. P2 Important issues not at the top of the work list r: fixed Issue is closed as already fixed in a newer version team-design Owned by Design Languages team triaged-design Triaged by Design Languages team
Projects
None yet
3 participants