← Back to Articles

Flutter Buttons: A Complete Guide to Interactive UI Elements

Flutter Buttons: A Complete Guide to Interactive UI Elements

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 TextButton OutlinedButton Common Flutter Button Types

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.

Enabled Disabled Loading Button States

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!