TextButton class
A Material Design "Text Button".
Use text buttons on toolbars, in dialogs, or inline with other content but offset from that content with padding so that the button's presence is obvious. Text buttons do not have visible borders and must therefore rely on their position relative to other content for context. In dialogs and cards, they should be grouped together in one of the bottom corners. Avoid using text buttons where they would blend in with other content, for example in the middle of lists.
A text button is a label child displayed on a (zero elevation) Material widget. The label's Text and Icon widgets are displayed in the style's ButtonStyle.foregroundColor. The button reacts to touches by filling with the style's ButtonStyle.backgroundColor.
The text button's default style is defined by defaultStyleOf. The style of this text button can be overridden with its style parameter. The style of all text buttons in a subtree can be overridden with the TextButtonTheme and the style of all of the text buttons in an app can be overridden with the Theme's ThemeData.textButtonTheme property.
The static styleFrom method is a convenient way to create a text button ButtonStyle from simple values.
If the onPressed and onLongPress callbacks are null, then this button will be disabled, it will not react to touch.
This sample shows various ways to configure TextButtons, from the simplest default appearance to versions that don't resemble Material Design at all.
To see it in action, copy and run this code snippet on DartPad.
import 'package:material_ui/material_ui.dart';
/// Flutter code sample for [TextButton].
void main() {
runApp(const TextButtonExampleApp());
}
class TextButtonExampleApp extends StatefulWidget {
const TextButtonExampleApp({super.key});
@override
State<TextButtonExampleApp> createState() => _TextButtonExampleAppState();
}
class _TextButtonExampleAppState extends State<TextButtonExampleApp> {
bool darkMode = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
themeMode: darkMode ? .dark : .light,
theme: ThemeData(brightness: .light),
darkTheme: ThemeData(brightness: .dark),
home: Scaffold(
body: Padding(
padding: const .all(16),
child: TextButtonExample(
darkMode: darkMode,
updateDarkMode: (bool value) {
setState(() {
darkMode = value;
});
},
),
),
),
);
}
}
class TextButtonExample extends StatefulWidget {
const TextButtonExample({
super.key,
required this.darkMode,
required this.updateDarkMode,
});
final bool darkMode;
final ValueChanged<bool> updateDarkMode;
@override
State<TextButtonExample> createState() => _TextButtonExampleState();
}
class _TextButtonExampleState extends State<TextButtonExample> {
TextDirection textDirection = .ltr;
ThemeMode themeMode = .light;
late final ScrollController scrollController;
Future<void>? currentAction;
static const Widget verticalSpacer = SizedBox(height: 16);
static const Widget horizontalSpacer = SizedBox(width: 32);
static const ImageProvider grassImage = NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_grass.jpeg',
);
static const ImageProvider defaultImage = NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_default.png',
);
static const ImageProvider hoveredImage = NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_hovered.png',
);
static const ImageProvider pressedImage = NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_pressed.png',
);
static const ImageProvider runningImage = NetworkImage(
'https://flutter.github.io/assets-for-api-docs/assets/material/text_button_nhu_end.png',
);
@override
void initState() {
scrollController = ScrollController();
super.initState();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
// Adapt colors that are not part of the color scheme to
// the current dark/light mode. Used to define TextButton #7's
// gradients.
final (
Color color1,
Color color2,
Color color3,
) = switch (colorScheme.brightness) {
.light => (Colors.blue, Colors.orange, Colors.yellow),
.dark => (Colors.purple, Colors.cyan, Colors.yellow),
};
// This gradient's appearance reflects the button's state.
// Always return a gradient decoration so that AnimatedContainer
// can interpolate in between. Used by TextButton #7.
Decoration? statesToDecoration(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return BoxDecoration(
gradient: LinearGradient(
colors: <Color>[color2, color2],
), // solid fill
);
}
return BoxDecoration(
gradient: LinearGradient(
colors: switch (states.contains(WidgetState.hovered)) {
true => <Color>[color1, color2],
false => <Color>[color2, color1],
},
),
);
}
// To make this method a little easier to read, the buttons that
// appear in the two columns to the right of the demo switches
// Card are broken out below.
final List<Widget> columnOneButtons = <Widget>[
TextButton(onPressed: () {}, child: const Text('Enabled')),
verticalSpacer,
const TextButton(onPressed: null, child: Text('Disabled')),
verticalSpacer,
TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.access_alarm),
label: const Text('TextButton.icon #1'),
),
verticalSpacer,
// Override the foreground and background colors.
//
// In this example, and most of the ones that follow, we're using
// the TextButton.styleFrom() convenience method to create a ButtonStyle.
// The styleFrom method is a little easier because it creates
// ButtonStyle WidgetStateProperty parameters for you.
// In this case, Specifying foregroundColor overrides the text,
// icon and overlay (splash and highlight) colors a little differently
// depending on the button's state. BackgroundColor is just the background
// color for all states.
TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: colorScheme.onError,
backgroundColor: colorScheme.error,
),
onPressed: () {},
icon: const Icon(Icons.access_alarm),
label: const Text('TextButton.icon #2'),
),
verticalSpacer,
// Override the button's shape and its border.
//
// In this case we've specified a shape that has border - the
// RoundedRectangleBorder's side parameter. If the styleFrom
// side parameter was also specified, or if the TextButtonTheme
// defined above included a side parameter, then that would
// override the RoundedRectangleBorder's side.
TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: const .all(Radius.circular(8)),
side: BorderSide(color: colorScheme.primary, width: 5),
),
),
onPressed: () {},
child: const Text('TextButton #3'),
),
verticalSpacer,
// Override overlay: the ink splash and highlight colors.
//
// The styleFrom method turns the specified overlayColor
// into a value MaterialStyleProperty<Color> ButtonStyle.overlay
// value that uses opacities depending on the button's state.
// If the overlayColor was Colors.transparent, no splash
// or highlights would be shown.
TextButton(
style: TextButton.styleFrom(overlayColor: Colors.yellow),
onPressed: () {},
child: const Text('TextButton #4'),
),
];
final List<Widget> columnTwoButtons = <Widget>[
// Override the foregroundBuilder: apply a ShaderMask.
//
// Apply a ShaderMask to the button's child. This kind of thing
// can be applied to one button easily enough by just wrapping the
// button's child directly. However to affect all buttons in this
// way you can specify a similar foregroundBuilder in a TextButton
// theme or the MaterialApp theme's ThemeData.textButtonTheme.
TextButton(
style: TextButton.styleFrom(
foregroundBuilder:
(BuildContext context, Set<WidgetState> states, Widget? child) {
return ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
colorScheme.primary,
colorScheme.onPrimary,
],
).createShader(bounds);
},
blendMode: BlendMode.srcATop,
child: child,
);
},
),
onPressed: () {},
child: const Text('TextButton #5'),
),
verticalSpacer,
// Override the foregroundBuilder: add an underline.
//
// Add a border around button's child. In this case the
// border only appears when the button is hovered or pressed
// (if it's pressed it's always hovered too). Not that this
// border is different than the one specified with the styleFrom
// side parameter (or the ButtonStyle.side property). The foregroundBuilder
// is applied to a widget that contains the child and has already
// included the button's padding. It is unaffected by the button's shape.
// The styleFrom side parameter controls the button's outermost border and it
// outlines the button's shape.
TextButton(
style: TextButton.styleFrom(
foregroundBuilder:
(BuildContext context, Set<WidgetState> states, Widget? child) {
return DecoratedBox(
decoration: BoxDecoration(
border: states.contains(WidgetState.hovered)
? Border(bottom: BorderSide(color: colorScheme.primary))
: const Border(), // essentially "no border"
),
child: child,
);
},
),
onPressed: () {},
child: const Text('TextButton #6'),
),
verticalSpacer,
// Override the backgroundBuilder to add a state specific gradient background
// and add an outline that only appears when the button is hovered or pressed.
//
// The gradient background decoration is computed by the statesToDecoration()
// method. The gradient flips horizontally when the button is hovered (watch
// closely). Because we want the outline to only appear when the button is hovered
// we can't use the styleFrom() side parameter, because that creates the same
// outline for all states. The ButtonStyle.copyWith() method is used to add
// a WidgetState<BorderSide?> property that does the right thing.
//
// The gradient background is translucent - all of the colors have opacity 0.5 -
// so the overlay's splash and highlight colors are visible even though they're
// drawn on the Material widget that's effectively behind the background. The
// border is also translucent, so if you look carefully, you'll see that the
// background - which is part of the button's Material but is drawn on top of the
// the background gradient - shows through the border.
TextButton(
onPressed: () {},
style:
TextButton.styleFrom(
overlayColor: color2,
backgroundBuilder:
(
BuildContext context,
Set<WidgetState> states,
Widget? child,
) {
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: statesToDecoration(states),
child: child,
);
},
).copyWith(
side: WidgetStateProperty.resolveWith<BorderSide?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.hovered)) {
return BorderSide(width: 3, color: color3);
}
return null; // defer to the default
}),
),
child: const Text('TextButton #7'),
),
verticalSpacer,
// Override the backgroundBuilder to add a grass image background.
//
// The image is clipped to the button's shape. We've included an Ink widget
// because the background image is opaque and would otherwise obscure the splash
// and highlight overlays that are painted on the button's Material widget
// by default. They're drawn on the Ink widget instead. The foreground color
// was overridden as well because white shows up a little better on the mottled
// green background.
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundBuilder:
(BuildContext context, Set<WidgetState> states, Widget? child) {
return Ink(
decoration: const BoxDecoration(
image: DecorationImage(image: grassImage, fit: .cover),
),
child: child,
);
},
),
child: const Text('TextButton #8'),
),
verticalSpacer,
// Override the foregroundBuilder to specify images for the button's pressed
// hovered and default states. We switch to an additional image while the
// button's callback is "running".
//
// This is an example of completely changing the default appearance of a button
// by specifying images for each state and by turning off the overlays by
// overlayColor: Colors.transparent. AnimatedContainer takes care of the
// fade in and out segues between images.
//
// This foregroundBuilder function ignores its child parameter. Unfortunately
// TextButton's child parameter is required, so we still have
// to provide one.
TextButton(
onPressed: () async {
// This is slightly complicated so that if the user presses the button
// while the current Future.delayed action is running, the currentAction
// flag is only reset to null after the _new_ action completes.
late final Future<void> thisAction;
thisAction = Future<void>.delayed(const Duration(seconds: 1), () {
if (currentAction == thisAction) {
setState(() {
currentAction = null;
});
}
});
setState(() {
currentAction = thisAction;
});
},
style: TextButton.styleFrom(
overlayColor: Colors.transparent,
foregroundBuilder:
(BuildContext context, Set<WidgetState> states, Widget? child) {
late final ImageProvider image;
if (currentAction != null) {
image = runningImage;
} else if (states.contains(WidgetState.pressed)) {
image = pressedImage;
} else if (states.contains(WidgetState.hovered)) {
image = hoveredImage;
} else {
image = defaultImage;
}
return AnimatedContainer(
width: 64,
height: 64,
duration: const Duration(milliseconds: 300),
curve: Curves.fastOutSlowIn,
decoration: BoxDecoration(
image: DecorationImage(image: image, fit: .contain),
),
);
},
),
child: const Text('This child is not used'),
),
];
return Row(
children: <Widget>[
// The dark/light and LTR/RTL switches. We use the updateDarkMode function
// provided by the parent TextButtonExampleApp to rebuild the MaterialApp
// in the appropriate dark/light ThemeMdoe. The directionality of the rest
// of the UI is controlled by the Directionality widget below, and the
// textDirection local state variable.
TextButtonExampleSwitches(
darkMode: widget.darkMode,
updateDarkMode: widget.updateDarkMode,
textDirection: textDirection,
updateRTL: (bool value) {
setState(() {
textDirection = value ? .rtl : .ltr;
});
},
),
horizontalSpacer,
Expanded(
child: Scrollbar(
controller: scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: scrollController,
child: Row(
mainAxisAlignment: .spaceEvenly,
mainAxisSize: .min,
children: <Widget>[
Directionality(
textDirection: textDirection,
child: Column(children: columnOneButtons),
),
horizontalSpacer,
Directionality(
textDirection: textDirection,
child: Column(children: columnTwoButtons),
),
horizontalSpacer,
],
),
),
),
),
],
);
}
}
class TextButtonExampleSwitches extends StatelessWidget {
const TextButtonExampleSwitches({
super.key,
required this.darkMode,
required this.updateDarkMode,
required this.textDirection,
required this.updateRTL,
});
final bool darkMode;
final ValueChanged<bool> updateDarkMode;
final TextDirection textDirection;
final ValueChanged<bool> updateRTL;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const .all(16),
child: IntrinsicWidth(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
const Expanded(child: Text('Dark Mode')),
const SizedBox(width: 4),
Switch(value: darkMode, onChanged: updateDarkMode),
],
),
const SizedBox(height: 16),
Row(
children: <Widget>[
const Expanded(child: Text('RTL Text')),
const SizedBox(width: 4),
Switch(value: textDirection == .rtl, onChanged: updateRTL),
],
),
],
),
),
),
);
}
}
This sample demonstrates using the statesController parameter to create a button that adds support for WidgetState.selected.
To see it in action, copy and run this code snippet on DartPad.
import 'package:material_ui/material_ui.dart';
/// Flutter code sample for [TextButton].
void main() {
runApp(const MaterialApp(home: Home()));
}
class SelectableButton extends StatefulWidget {
const SelectableButton({
super.key,
required this.selected,
this.style,
required this.onPressed,
required this.child,
});
final bool selected;
final ButtonStyle? style;
final VoidCallback? onPressed;
final Widget child;
@override
State<SelectableButton> createState() => _SelectableButtonState();
}
class _SelectableButtonState extends State<SelectableButton> {
late final WidgetStatesController statesController;
@override
void initState() {
super.initState();
statesController = WidgetStatesController(<WidgetState>{
if (widget.selected) WidgetState.selected,
});
}
@override
void didUpdateWidget(SelectableButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selected != oldWidget.selected) {
statesController.update(WidgetState.selected, widget.selected);
}
}
@override
Widget build(BuildContext context) {
return TextButton(
statesController: statesController,
style: widget.style,
onPressed: widget.onPressed,
child: widget.child,
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool selected = false;
/// Sets the button's foreground and background colors.
/// If not selected, resolves to null and defers to default values.
static const ButtonStyle style = ButtonStyle(
foregroundColor: WidgetStateProperty<Color?>.fromMap(<WidgetState, Color>{
WidgetState.selected: Colors.white,
}),
backgroundColor: WidgetStateProperty<Color?>.fromMap(<WidgetState, Color>{
WidgetState.selected: Colors.indigo,
}),
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SelectableButton(
selected: selected,
style: style,
onPressed: () {
setState(() {
selected = !selected;
});
},
child: const Text('toggle selected'),
),
),
);
}
}
See also:
- ElevatedButton, a filled button whose material elevates when pressed.
- FilledButton, a filled button that doesn't elevate when pressed.
- FilledButton.tonal, a filled button variant that uses a secondary fill color.
- OutlinedButton, a button with an outlined border and no fill color.
- material.io/design/components/buttons.html
- m3.material.io/components/buttons
- Inheritance
Constructors
-
TextButton({Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, ValueChanged<
bool> ? onHover, ValueChanged<bool> ? onFocusChange, ButtonStyle? style, FocusNode? focusNode, bool autofocus = false, Clip? clipBehavior, MaterialStatesController? statesController, bool? isSemanticButton = true, required Widget child}) -
Create a TextButton.
const
-
TextButton.icon({Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, ValueChanged<
bool> ? onHover, ValueChanged<bool> ? onFocusChange, ButtonStyle? style, FocusNode? focusNode, bool autofocus = false, Clip? clipBehavior = Clip.none, MaterialStatesController? statesController, Widget? icon, required Widget label, IconAlignment? iconAlignment}) -
Create a text button from a pair of widgets that serve as the button's
iconandlabel.
Properties
- autofocus → bool
-
True if this widget will be selected as the initial focus when no other
node in its scope is currently focused.
finalinherited
- child → Widget?
-
Typically the button's label.
finalinherited
- clipBehavior → Clip?
-
The content will be clipped (or not) according to this option.
finalinherited
- enabled → bool
-
Whether the button is enabled or disabled.
no setterinherited
- focusNode → FocusNode?
-
An optional focus node to use as the focus node for this widget.
finalinherited
- hashCode → int
-
The hash code for this object.
no setterinherited
- iconAlignment → IconAlignment?
-
Determines the alignment of the icon within the widgets such as:
finalinherited
- isSemanticButton → bool?
-
Determine whether this subtree represents a button.
finalinherited
- key → Key?
-
Controls how one widget replaces another widget in the tree.
finalinherited
-
onFocusChange
→ ValueChanged<
bool> ? -
Handler called when the focus changes.
finalinherited
-
onHover
→ ValueChanged<
bool> ? -
Called when a pointer enters or exits the button response area.
finalinherited
- onLongPress → VoidCallback?
-
Called when the button is long-pressed.
finalinherited
- onPressed → VoidCallback?
-
Called when the button is tapped or otherwise activated.
finalinherited
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
- statesController → MaterialStatesController?
-
Represents the interactive "state" of this widget in terms of
a set of WidgetStates, like WidgetState.pressed and
WidgetState.focused.
finalinherited
- style → ButtonStyle?
-
Customizes this button's appearance.
finalinherited
- tooltip → String?
-
Text that describes the action that will occur when the button is pressed or
hovered over.
finalinherited
Methods
-
createElement(
) → StatefulElement -
Creates a StatefulElement to manage this widget's location in the tree.
inherited
-
createState(
) → State< ButtonStyleButton> -
Creates the mutable state for this widget at a given location in the tree.
inherited
-
debugDescribeChildren(
) → List< DiagnosticsNode> -
Returns a list of DiagnosticsNode objects describing this node's
children.
inherited
-
debugFillProperties(
DiagnosticPropertiesBuilder properties) → void -
Add additional properties associated with the node.
inherited
-
defaultStyleOf(
BuildContext context) → ButtonStyle -
Defines the button's default appearance.
override
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
themeStyleOf(
BuildContext context) → ButtonStyle? -
Returns the TextButtonThemeData.style of the closest
TextButtonTheme ancestor.
override
-
toDiagnosticsNode(
{String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode -
Returns a debug representation of the object that is used by debugging
tools and by DiagnosticsNode.toStringDeep.
inherited
-
toString(
{DiagnosticLevel minLevel = DiagnosticLevel.info}) → String -
A string representation of this object.
inherited
-
toStringDeep(
{String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) → String -
Returns a string representation of this node and its descendants.
inherited
-
toStringShallow(
{String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String -
Returns a one-line detailed description of the object.
inherited
-
toStringShort(
) → String -
A short, textual description of this widget.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited
Static Methods
-
styleFrom(
{Color? foregroundColor, Color? backgroundColor, Color? disabledForegroundColor, Color? disabledBackgroundColor, Color? shadowColor, Color? surfaceTintColor, Color? iconColor, double? iconSize, IconAlignment? iconAlignment, Color? disabledIconColor, Color? overlayColor, double? elevation, TextStyle? textStyle, EdgeInsetsGeometry? padding, Size? minimumSize, Size? fixedSize, Size? maximumSize, BorderSide? side, OutlinedBorder? shape, MouseCursor? enabledMouseCursor, MouseCursor? disabledMouseCursor, VisualDensity? visualDensity, MaterialTapTargetSize? tapTargetSize, Duration? animationDuration, bool? enableFeedback, AlignmentGeometry? alignment, InteractiveInkFeatureFactory? splashFactory, ButtonLayerBuilder? backgroundBuilder, ButtonLayerBuilder? foregroundBuilder}) → ButtonStyle - A static convenience method that constructs a text button ButtonStyle given simple values.