Flutter Buttons: A Complete Guide to Interactive UI Elements
Buttons are the workhorses of mobile app interfaces. They invite users to take action, submit forms, navigate between screens, and interact with your app in meaningful ways. Flutter provides a rich set of button widgets, each designed for specific use cases and visual styles.
In this guide, we'll explore Flutter's button ecosystem, learn when to use each type, and discover how to customize them to match your app's design language. Whether you're building your first Flutter app or looking to refine your UI components, understanding buttons is essential.
Understanding Flutter's Button Widgets
Flutter offers several button types, each with distinct visual characteristics and use cases. The main button widgets are:
- ElevatedButton - Raised buttons with shadow, perfect for primary actions
- TextButton - Minimal text-only buttons, great for secondary actions
- OutlinedButton - Buttons with borders, ideal for outlined styles
- IconButton - Icon-only buttons, perfect for toolbars and app bars
- FloatingActionButton - Circular floating buttons for primary actions
ElevatedButton: Your Primary Action Button
ElevatedButton is Flutter's go-to widget for primary actions. It features a raised appearance with a shadow, making it stand out from the background. This visual prominence signals to users that this is the main action they should take.
Here's a basic example of using ElevatedButton:
ElevatedButton(
onPressed: () {
print('Button pressed!');
},
child: Text('Submit'),
)
The onPressed callback is where you define what happens when the button is tapped. If you set onPressed to null, the button becomes disabled and appears grayed out.
You can customize ElevatedButton 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(12),
),
),
child: Text('Custom Button'),
)
TextButton: Minimal and Clean
TextButton is perfect for secondary actions that don't need as much visual emphasis. It's essentially text with a tap target, making it ideal for cancel buttons, "Learn more" links, or actions that are available but not primary.
TextButton(
onPressed: () {
Navigator.pop();
},
child: Text('Cancel'),
)
TextButton can be styled similarly to ElevatedButton:
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.all(16),
),
child: Text('Learn More'),
)
OutlinedButton: The Best of Both Worlds
OutlinedButton combines the visual weight of ElevatedButton with the subtlety of TextButton. It features a border outline and transparent fill, making it perfect for actions that are important but secondary to the primary action.
OutlinedButton(
onPressed: () {
// Handle action
},
child: Text('Save Draft'),
)
OutlinedButton works great in forms where you have a primary submit button and a secondary "save draft" option:
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
onPressed: () {
// Save draft logic
},
child: Text('Save Draft'),
),
ElevatedButton(
onPressed: () {
// Submit logic
},
child: Text('Submit'),
),
],
)
IconButton: Compact and Iconic
IconButton is perfect for toolbars, app bars, and situations where space is limited. It displays only an icon, making it compact and recognizable.
IconButton(
onPressed: () {
// Handle icon tap
},
icon: Icon(Icons.favorite),
tooltip: 'Add to favorites',
)
The tooltip parameter is important for accessibility - it provides a description when users long-press the button or use screen readers.
You can combine icons with text in other button types too:
ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.download),
label: Text('Download'),
)
FloatingActionButton: The Circular Hero
FloatingActionButton (FAB) is Flutter's signature floating button. It's typically circular, floats above the content, and represents the primary action on a screen. The Material Design guidelines suggest using it for the most common action.
Scaffold(
body: Center(
child: Text('Your content here'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Primary action
},
child: Icon(Icons.add),
),
)
FloatingActionButton comes in different sizes:
FloatingActionButton(
onPressed: () {},
mini: true, // Smaller version
child: Icon(Icons.edit),
)
FloatingActionButton.extended(
onPressed: () {},
icon: Icon(Icons.share),
label: Text('Share'),
)
Button States: Enabled, Disabled, and Loading
Buttons can exist in different states, and Flutter makes it easy to handle these states gracefully. The most common states are enabled, disabled, and loading.
Here's how to implement these states:
class MyButton extends StatefulWidget {
@override
_MyButtonState createState() => _MyButtonState();
}
class _MyButtonState extends State {
bool _isLoading = false;
Future _handleSubmit() async {
setState(() {
_isLoading = true;
});
try {
await Future.delayed(Duration(seconds: 2)); // Simulate API call
// Handle success
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _isLoading ? null : _handleSubmit,
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: Text('Submit'),
);
}
}
Button Theming: Consistent Design Across Your App
Instead of styling each button individually, you can define button themes at the app level. This ensures consistency and makes it easier to maintain your design system.
MaterialApp(
theme: ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
),
),
home: MyHomePage(),
)
With this setup, all ElevatedButton widgets in your app will automatically use these styles unless you override them locally.
Custom Buttons: Building Your Own
Sometimes, you need a button that doesn't fit into Flutter's standard button types. You can create custom buttons by combining existing widgets or building from scratch.
class CustomGradientButton extends StatelessWidget {
final VoidCallback? onPressed;
final String text;
const CustomGradientButton({
Key? key,
required this.onPressed,
required this.text,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(12),
),
child: Text(
text,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
This custom button uses InkWell for tap handling and Container for styling. The InkWell provides Material Design ripple effects automatically.
Best Practices for Flutter Buttons
Here are some guidelines to help you use buttons effectively in your Flutter apps:
- Be consistent - Use the same button style for similar actions throughout your app
- Provide feedback - Always show loading states or disable buttons during async operations
- Size appropriately - Buttons should be at least 48x48 pixels for easy tapping
- Use clear labels - Button text should clearly indicate what action will occur
- Consider accessibility - Add tooltips and ensure sufficient color contrast
- Limit primary actions - Each screen should have one clear primary action
Common Patterns and Examples
Let's look at a practical example that combines multiple button types in a form:
class ContactForm extends StatefulWidget {
@override
_ContactFormState createState() => _ContactFormState();
}
class _ContactFormState extends State {
final _formKey = GlobalKey();
bool _isSubmitting = false;
Future _submitForm() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isSubmitting = true;
});
// Simulate form submission
await Future.delayed(Duration(seconds: 2));
setState(() {
_isSubmitting = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Form submitted successfully!')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Contact Us'),
actions: [
IconButton(
icon: Icon(Icons.help),
onPressed: () {
// Show help dialog
},
tooltip: 'Help',
),
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isSubmitting ? null : () {
Navigator.pop(context);
},
child: Text('Cancel'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: _isSubmitting ? null : _submitForm,
child: _isSubmitting
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: Text('Submit'),
),
],
),
],
),
),
),
);
}
}
This example demonstrates several button concepts: using IconButton in the app bar, TextButton for cancel, ElevatedButton for submit, and handling loading states properly.
Conclusion
Buttons are fundamental to creating interactive Flutter applications. By understanding the different button types and their appropriate use cases, you can build interfaces that guide users naturally through your app's functionality. Remember to keep your button usage consistent, provide clear feedback, and always consider accessibility.
As you continue building Flutter apps, experiment with button styling and theming to create a unique look that matches your brand. The flexibility of Flutter's button system allows you to create beautiful, functional interfaces while maintaining the Material Design principles that users expect.
Happy coding, and may your buttons always be tapped!