Flutter Buttons: Understanding ElevatedButton, TextButton, and IconButton
Buttons are one of the most fundamental UI components in any mobile app. In Flutter, buttons come in several flavors, each designed for specific use cases. Whether you're building a simple form or a complex dashboard, understanding the different button types and when to use them is crucial for creating intuitive user interfaces.
In this article, we'll explore Flutter's three main button widgets: ElevatedButton, TextButton, and IconButton. We'll learn what makes each unique, when to use them, and how to customize them to fit your app's design.
Why Multiple Button Types?
Before diving into the specifics, you might wonder why Flutter provides multiple button types instead of one configurable button. The answer lies in Material Design principles and user experience best practices. Different visual styles communicate different levels of importance and action types to users.
Think of it like this: a prominent raised button draws attention for primary actions, while a subtle text button works well for secondary actions that shouldn't compete for attention. Icon buttons are perfect for toolbars and compact spaces where text would be too verbose.
ElevatedButton: Your Primary Action Hero
ElevatedButton is Flutter's go-to widget for primary actions—the main things you want users to do. It has a raised appearance with a shadow, making it stand out from the background. This visual prominence signals importance.
Here's a basic example:
ElevatedButton(
onPressed: () {
print('Button pressed!');
},
child: Text('Submit'),
)
The ElevatedButton requires an `onPressed` callback (which can be null to disable the button) and a `child` widget, typically a Text widget. When `onPressed` is null, the button automatically becomes disabled and visually dimmed.
One of the great things about ElevatedButton is its built-in Material Design styling. It automatically adapts to your app's theme, using your primary color scheme. But you can also customize it extensively:
ElevatedButton(
onPressed: () {
// Handle button press
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text('Custom Button'),
)
In this example, we're customizing the background color, text color, padding, and border radius. The `styleFrom` method creates a ButtonStyle based on the provided properties, which is cleaner than manually constructing a ButtonStyle object.
TextButton: Subtle and Secondary
TextButton is perfect for secondary actions—things that are available but not the primary focus. It has no background or elevation, just text that responds to taps. This makes it less visually prominent than ElevatedButton, which is exactly what you want for secondary actions.
Common use cases include "Cancel" buttons, "Learn More" links, or actions in dialogs where the primary action should take center stage.
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Cancel'),
)
Like ElevatedButton, TextButton can be customized:
TextButton(
onPressed: () {
// Handle action
},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.all(16),
),
child: Text('Learn More'),
)
Notice that TextButton uses `foregroundColor` instead of `backgroundColor` since it doesn't have a background. The foreground color affects both the text and the ripple effect when tapped.
IconButton: Compact and Iconic
IconButton is designed for icons rather than text. It's perfect for toolbars, app bars, and places where space is limited. Instead of a child widget, it takes an `icon` parameter.
IconButton(
onPressed: () {
// Handle icon tap
},
icon: Icon(Icons.favorite),
tooltip: 'Add to favorites',
)
The `tooltip` parameter is especially important for IconButton since icons can be ambiguous. When users long-press or hover (on web/desktop), they'll see the tooltip explaining what the button does.
IconButton also supports customization:
IconButton(
onPressed: () {
// Handle action
},
icon: Icon(Icons.share),
color: Colors.blue,
iconSize: 28,
tooltip: 'Share',
)
Button States: Enabled, Disabled, and Loading
All three button types handle disabled states gracefully. When `onPressed` is null, the button becomes disabled:
ElevatedButton(
onPressed: isLoading ? null : () {
// Handle submission
},
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text('Submit'),
)
In this example, we're conditionally disabling the button and showing a loading indicator when `isLoading` is true. This is a common pattern for form submissions where you want to prevent multiple submissions and provide visual feedback.
Real-World Example: A Login Form
Let's put it all together in a practical example—a login form that uses all three button types appropriately:
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
bool _obscurePassword = true;
Future<void> _handleLogin() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
// Simulate API call
await Future.delayed(Duration(seconds: 2));
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
tooltip: _obscurePassword ? 'Show password' : 'Hide password',
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 48),
),
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text('Login'),
),
SizedBox(height: 16),
TextButton(
onPressed: () {
// Navigate to forgot password screen
},
child: Text('Forgot Password?'),
),
],
),
);
}
}
In this example:
- ElevatedButton is used for the primary "Login" action—it's prominent and spans the full width
- IconButton is used in the password field's suffix to toggle visibility—compact and icon-based
- TextButton is used for the secondary "Forgot Password?" action—subtle and doesn't compete with the primary action
Button Groups and Layouts
Often, you'll need to place multiple buttons together. Flutter provides several layout widgets that work great with buttons:
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Cancel'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () {
// Handle save
},
child: Text('Save'),
),
],
)
This is a common pattern in dialogs where you have a cancel action (TextButton) and a confirm action (ElevatedButton). The Row widget arranges them horizontally, and MainAxisAlignment.end aligns them to the right.
Accessibility Considerations
When working with buttons, especially IconButton, always consider accessibility:
- Always provide tooltips for IconButton widgets
- Use semantic labels for screen readers
- Ensure sufficient color contrast
- Make sure buttons are large enough to tap easily (minimum 48x48 logical pixels)
Semantics(
label: 'Delete item',
button: true,
child: IconButton(
onPressed: () {
// Delete action
},
icon: Icon(Icons.delete),
tooltip: 'Delete',
),
)
Custom Button Styles with Theme
Instead of styling each button individually, you can define button styles globally using Theme:
MaterialApp(
theme: ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
),
),
home: MyHomePage(),
)
This approach ensures consistency across your app and makes it easier to maintain a cohesive design system.
Conclusion
Understanding Flutter's button widgets is essential for building intuitive user interfaces. ElevatedButton draws attention for primary actions, TextButton provides subtle secondary actions, and IconButton offers compact icon-based interactions. Each serves a specific purpose in Material Design, and choosing the right one helps users understand the hierarchy and importance of actions in your app.
Remember to consider button states (enabled, disabled, loading), accessibility, and consistent styling. With these three button types and Flutter's theming system, you have everything you need to create beautiful, functional buttons that guide users through your app's interface.
Happy coding, and may your buttons always be pressed at the right moments!