Flutter Widgets Fundamentals

Button Widgets

45 min Lesson 5 of 18

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'),
  ),
)
Note: In Material 3 (Flutter 3.x default), 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),
)
Tip: The extended FAB is great for screens where the primary action might not be obvious from just an icon. Combine 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'),
)
Warning: In Flutter 3.22+, 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
What’s Next: In the upcoming lessons, we will explore layout widgets like Container, Row, Column, Stack, and more to learn how to arrange and position widgets on screen.