Button Widgets
Button Widgets in Flutter
Buttons are the primary way users interact with your app. Flutter provides a rich set of Material Design button widgets, each designed for specific use cases. In Flutter 3.x, the button system is built around three core buttons (ElevatedButton, TextButton, OutlinedButton) with consistent styling via ButtonStyle.
ElevatedButton
The ElevatedButton is a filled button with elevation (shadow). It is the most prominent button type and is used for primary actions that you want users to notice first.
ElevatedButton Examples
// Basic elevated button
ElevatedButton(
onPressed: () {
debugPrint('Button pressed!');
},
child: const Text('Submit'),
)
// With icon
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.send),
label: const Text('Send Message'),
)
// Styled elevated button
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: const Text('Get Started'),
)
// Full-width button
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: () {},
child: const Text('Continue'),
),
)
ElevatedButton has a tonal color scheme by default. Use ElevatedButton.styleFrom() to customize colors. The old RaisedButton is deprecated — always use ElevatedButton instead.TextButton
A TextButton is a flat button with no elevation or border. It is used for less prominent actions, especially in dialogs, cards, and toolbars where a more subtle appearance is appropriate.
TextButton Examples
// Basic text button
TextButton(
onPressed: () {},
child: const Text('Learn More'),
)
// With icon
TextButton.icon(
onPressed: () {},
icon: const Icon(Icons.arrow_forward),
label: const Text('Next'),
)
// Styled text button
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text('Delete Account'),
)
// In a dialog
AlertDialog(
title: const Text('Confirm'),
content: const Text('Are you sure you want to continue?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
// Confirm action
Navigator.pop(context);
},
child: const Text('Confirm'),
),
],
)
OutlinedButton
An OutlinedButton has a border but no fill color and no elevation. It sits between ElevatedButton (high emphasis) and TextButton (low emphasis) in terms of visual prominence. Use it for secondary actions.
OutlinedButton Examples
// Basic outlined button
OutlinedButton(
onPressed: () {},
child: const Text('View Details'),
)
// With icon
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.download),
label: const Text('Download'),
)
// Custom styled
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.green,
side: const BorderSide(color: Colors.green, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
),
child: const Text('Add to Cart'),
)
IconButton
We covered IconButton in the previous lesson for icon interactions. Here is a quick recap with additional styling patterns available in Material 3:
IconButton Variants (Material 3)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Standard
IconButton(
onPressed: () {},
icon: const Icon(Icons.edit),
tooltip: 'Edit',
),
// Filled
IconButton.filled(
onPressed: () {},
icon: const Icon(Icons.add),
),
// Filled tonal
IconButton.filledTonal(
onPressed: () {},
icon: const Icon(Icons.bookmark),
),
// Outlined
IconButton.outlined(
onPressed: () {},
icon: const Icon(Icons.share),
),
],
)
FloatingActionButton
The FloatingActionButton (FAB) is a circular button that floats above the content. It represents the primary action of a screen. Use it sparingly — typically one per screen.
FloatingActionButton Examples
// In a Scaffold
Scaffold(
appBar: AppBar(title: const Text('FAB Demo')),
body: const Center(child: Text('Content')),
// Standard circular FAB
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Add',
child: const Icon(Icons.add),
),
)
// Small FAB
FloatingActionButton.small(
onPressed: () {},
child: const Icon(Icons.add),
)
// Large FAB
FloatingActionButton.large(
onPressed: () {},
child: const Icon(Icons.add),
)
// Extended FAB with label
FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('New Post'),
)
// Custom styled FAB
FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.chat),
)
FloatingActionButton.extended with floatingActionButtonLocation on the Scaffold to control its position.DropdownButton
The DropdownButton lets users select one value from a list of options. It shows the currently selected value and opens a dropdown menu when tapped.
DropdownButton Example
class LanguageSelector extends StatefulWidget {
const LanguageSelector({super.key});
@override
State<LanguageSelector> createState() => _LanguageSelectorState();
}
class _LanguageSelectorState extends State<LanguageSelector> {
String _selectedLanguage = 'English';
final List<String> _languages = [
'English', 'Arabic', 'Spanish', 'French', 'German',
];
@override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: _selectedLanguage,
icon: const Icon(Icons.arrow_drop_down),
isExpanded: true,
underline: Container(height: 2, color: Colors.blue),
items: _languages.map((lang) => DropdownMenuItem(
value: lang,
child: Text(lang),
)).toList(),
onChanged: (value) {
if (value != null) {
setState(() => _selectedLanguage = value);
}
},
);
}
}
// DropdownButtonFormField for forms
DropdownButtonFormField<String>(
value: _selectedLanguage,
decoration: const InputDecoration(
labelText: 'Language',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.language),
),
items: _languages.map((lang) => DropdownMenuItem(
value: lang,
child: Text(lang),
)).toList(),
onChanged: (value) {
setState(() => _selectedLanguage = value!);
},
validator: (value) => value == null ? 'Please select a language' : null,
)
PopupMenuButton
PopupMenuButton shows a menu of options when pressed. It is commonly used for overflow menus (the three-dot menu) in app bars and list items.
PopupMenuButton Example
PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'edit':
debugPrint('Edit selected');
break;
case 'delete':
debugPrint('Delete selected');
break;
case 'share':
debugPrint('Share selected');
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: ListTile(
leading: Icon(Icons.edit),
title: Text('Edit'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuItem(
value: 'share',
child: ListTile(
leading: Icon(Icons.share),
title: Text('Share'),
contentPadding: EdgeInsets.zero,
),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'delete',
child: ListTile(
leading: Icon(Icons.delete, color: Colors.red),
title: Text('Delete', style: TextStyle(color: Colors.red)),
contentPadding: EdgeInsets.zero,
),
),
],
icon: const Icon(Icons.more_vert),
)
ButtonStyle — Consistent Styling
ButtonStyle is the unified styling system for all Material buttons. Each button type has a styleFrom() factory method for convenient styling, but you can also create ButtonStyle objects directly for more control.
ButtonStyle Deep Dive
// Using styleFrom (convenient)
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey.shade300,
disabledForegroundColor: Colors.grey.shade500,
elevation: 4,
shadowColor: Colors.indigo.withOpacity(0.4),
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
minimumSize: const Size(120, 48),
maximumSize: const Size(300, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
child: const Text('Styled Button'),
)
// Using ButtonStyle directly (full control)
ElevatedButton(
onPressed: () {},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.pressed)) {
return Colors.indigo.shade700;
}
if (states.contains(WidgetState.hovered)) {
return Colors.indigo.shade600;
}
if (states.contains(WidgetState.disabled)) {
return Colors.grey.shade300;
}
return Colors.indigo;
}),
foregroundColor: WidgetStateProperty.all(Colors.white),
overlayColor: WidgetStateProperty.all(
Colors.white.withOpacity(0.1),
),
animationDuration: const Duration(milliseconds: 200),
),
child: const Text('Dynamic Style'),
)
MaterialStateProperty was renamed to WidgetStateProperty and MaterialState to WidgetState. The old names still work but are deprecated. Always use the new names in new code.Disabled State
A button is disabled when its onPressed (and onLongPress) callbacks are null. Flutter automatically applies disabled styling — reduced opacity and no ink splash.
Disabled Button Patterns
class FormSubmitButton extends StatefulWidget {
const FormSubmitButton({super.key});
@override
State<FormSubmitButton> createState() => _FormSubmitButtonState();
}
class _FormSubmitButtonState extends State<FormSubmitButton> {
bool _isValid = false;
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
SwitchListTile(
title: const Text('Form is valid'),
value: _isValid,
onChanged: (v) => setState(() => _isValid = v),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
// null = disabled, function = enabled
onPressed: (_isValid && !_isLoading)
? () async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2));
if (mounted) setState(() => _isLoading = false);
}
: null,
child: _isLoading
? const SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white,
),
)
: const Text('Submit'),
),
),
],
);
}
}
onPressed vs onLongPress
Most buttons support both onPressed (tap) and onLongPress (long press) callbacks. Use onLongPress for secondary actions or to show additional options.
Press and Long Press
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tapped!')),
);
},
onLongPress: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Long Press Detected'),
content: const Text('You long-pressed the button!'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
},
child: const Text('Tap or Long Press Me'),
)
Practical Example: Action Bar
A common UI pattern is an action bar with multiple button types:
Action Bar Widget
class ActionBar extends StatelessWidget {
final VoidCallback? onSave;
final VoidCallback? onCancel;
final VoidCallback? onDelete;
final bool isLoading;
const ActionBar({
super.key,
this.onSave,
this.onCancel,
this.onDelete,
this.isLoading = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
// Delete button (destructive)
if (onDelete != null)
OutlinedButton.icon(
onPressed: isLoading ? null : onDelete,
icon: const Icon(Icons.delete_outline),
label: const Text('Delete'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
),
const Spacer(),
// Cancel button
TextButton(
onPressed: isLoading ? null : onCancel,
child: const Text('Cancel'),
),
const SizedBox(width: 12),
// Save button (primary)
ElevatedButton.icon(
onPressed: isLoading ? null : onSave,
icon: isLoading
? const SizedBox(
width: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.check),
label: Text(isLoading ? 'Saving...' : 'Save'),
),
],
),
);
}
}
Practical Example: Styled Button Set
Build a complete set of themed buttons for a consistent app design:
Themed Button Set
class AppButton extends StatelessWidget {
final String label;
final IconData? icon;
final VoidCallback? onPressed;
final AppButtonVariant variant;
final bool isLoading;
final bool isFullWidth;
const AppButton({
super.key,
required this.label,
this.icon,
this.onPressed,
this.variant = AppButtonVariant.primary,
this.isLoading = false,
this.isFullWidth = false,
});
@override
Widget build(BuildContext context) {
final button = switch (variant) {
AppButtonVariant.primary => ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _buildChild(),
),
AppButtonVariant.secondary => OutlinedButton(
onPressed: isLoading ? null : onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: const BorderSide(color: Colors.blue),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _buildChild(),
),
AppButtonVariant.text => TextButton(
onPressed: isLoading ? null : onPressed,
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
),
child: _buildChild(),
),
AppButtonVariant.danger => ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _buildChild(),
),
};
return isFullWidth
? SizedBox(width: double.infinity, child: button)
: button;
}
Widget _buildChild() {
if (isLoading) {
return const SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
);
}
if (icon != null) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Text(label),
],
);
}
return Text(label);
}
}
enum AppButtonVariant { primary, secondary, text, danger }
// Usage
Column(
children: [
AppButton(label: 'Save Changes', icon: Icons.save, onPressed: () {}),
const SizedBox(height: 8),
AppButton(label: 'Export', variant: AppButtonVariant.secondary, onPressed: () {}),
const SizedBox(height: 8),
AppButton(label: 'Skip', variant: AppButtonVariant.text, onPressed: () {}),
const SizedBox(height: 8),
AppButton(label: 'Delete', variant: AppButtonVariant.danger, icon: Icons.delete, onPressed: () {}),
const SizedBox(height: 8),
AppButton(label: 'Full Width', isFullWidth: true, onPressed: () {}),
],
)
Summary
In this lesson, you learned:
- ElevatedButton for primary, high-emphasis actions with elevation and fill
- TextButton for low-emphasis actions in dialogs, cards, and toolbars
- OutlinedButton for medium-emphasis secondary actions with a border
- IconButton with Material 3 variants: filled, filledTonal, outlined
- FloatingActionButton for the primary action of a screen (small, regular, large, extended)
- DropdownButton for selecting from a list; PopupMenuButton for overflow menus
- ButtonStyle and
styleFrom()for consistent, state-aware button styling - Buttons are disabled when onPressed is null; use onLongPress for secondary interactions