← Back to Articles

Flutter Buttons: Understanding Button Widgets and Customization

Flutter Buttons: Understanding Button Widgets and Customization

Flutter Buttons: Understanding Button Widgets and Customization

Buttons are one of the most fundamental UI components in any mobile app. They invite users to take action, navigate between screens, and interact with your application. Flutter provides a rich set of button widgets, each designed for specific use cases and visual styles. Understanding when and how to use each button type will help you create intuitive, beautiful interfaces that users love.

In this article, we'll explore Flutter's button ecosystem, learn how to customize buttons to match your app's design, and discover best practices for creating accessible and performant button interactions.

The Button Family Tree

Flutter offers several button widgets, each with its own personality and purpose. Let's start by understanding the main button types available:

  • ElevatedButton - Raised buttons with shadow, perfect for primary actions
  • TextButton - Flat buttons with text only, great for secondary actions
  • OutlinedButton - Buttons with borders, ideal for outlined style actions
  • IconButton - Icon-only buttons, perfect for toolbars and compact spaces
  • FloatingActionButton - Circular floating buttons for primary actions
Flutter Button Types ElevatedButton TextButton OutlinedButton IconButton + FAB

ElevatedButton: Your Primary Action Hero

ElevatedButton is the go-to choice for primary actions in your app. It has a raised appearance with a shadow, making it stand out and clearly indicating the most important action users should take.


ElevatedButton(
  onPressed: () {
    print('Button pressed!');
  },
  child: Text('Sign In'),
)

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, which is perfect for form validation scenarios.


ElevatedButton(
  onPressed: isFormValid ? () {
    submitForm();
  } : null,
  child: Text('Submit'),
)

TextButton: Subtle but Effective

TextButton provides a flat, text-only button style. It's perfect for secondary actions that don't need as much visual emphasis. Think "Cancel" buttons, "Skip" options, or less critical actions.


TextButton(
  onPressed: () {
    Navigator.pop(context);
  },
  child: Text('Cancel'),
)

TextButton is also great for creating button groups or action rows where you want multiple options without overwhelming the user with too many prominent buttons.

OutlinedButton: The Best of Both Worlds

OutlinedButton combines the visual weight of ElevatedButton with the subtlety of TextButton. It has a border but no fill, making it perfect for actions that are important but not primary.


OutlinedButton(
  onPressed: () {
    showDialog(context: context, builder: (_) => AlertDialog(...));
  },
  child: Text('Learn More'),
)

IconButton: Compact and Iconic

IconButton is perfect for toolbars, app bars, and places where space is limited. It displays only an icon, making it ideal for actions like favorites, share, or settings.


IconButton(
  onPressed: () {
    toggleFavorite();
  },
  icon: Icon(Icons.favorite),
  tooltip: 'Add to favorites',
)

Always include a tooltip for IconButton to improve accessibility. Screen readers and users hovering over the button will see helpful descriptions.

FloatingActionButton: The Floating Star

FloatingActionButton (FAB) is a circular button that floats above the content. It's typically used for the primary action on a screen, like adding a new item or composing a message.


FloatingActionButton(
  onPressed: () {
    Navigator.push(context, MaterialPageRoute(
      builder: (_) => CreateItemScreen(),
    ));
  },
  child: Icon(Icons.add),
  tooltip: 'Add new item',
)

Customizing Button Appearance

All Flutter buttons can be extensively customized using the style parameter. You can change colors, padding, shape, and more to match your app's design system.

Styling ElevatedButton


ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.purple,
    foregroundColor: Colors.white,
    padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    elevation: 4,
  ),
  child: Text('Custom Button'),
)

Styling TextButton


TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.blue,
    padding: EdgeInsets.all(16),
    shape: StadiumBorder(),
  ),
  child: Text('Rounded Text Button'),
)

Creating Custom Button Shapes

You can create buttons with custom shapes using the shape parameter. This is perfect for creating pill-shaped buttons, buttons with sharp corners, or even circular buttons.


ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    shape: StadiumBorder(), // Pill shape
  ),
  child: Text('Pill Button'),
)

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    shape: CircleBorder(), // Circular
    padding: EdgeInsets.all(20),
  ),
  child: Icon(Icons.check),
)
Button Customization Options Custom Color Pill Shape Default Sharp Corners Outlined Colors Shapes Styles

Button States: Enabled, Disabled, and Loading

Real-world apps need to handle different button states gracefully. Let's explore how to manage enabled, disabled, and loading states effectively.

Disabled State


ElevatedButton(
  onPressed: isEmailValid && isPasswordValid ? () {
    login();
  } : null,
  child: Text('Login'),
)

When onPressed is null, Flutter automatically styles the button as disabled. This is perfect for form validation.

Loading State

For async operations, you'll want to show a loading indicator. Here's a common pattern:


ElevatedButton(
  onPressed: isLoading ? null : () async {
    setState(() => isLoading = true);
    try {
      await submitData();
    } finally {
      setState(() => isLoading = false);
    }
  },
  child: isLoading
    ? SizedBox(
        width: 20,
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 2,
          valueColor: AlwaysStoppedAnimation(Colors.white),
        ),
      )
    : Text('Submit'),
)

Button Sizes: Small, Medium, and Large

Flutter buttons support different sizes through the style parameter. You can create consistent sizing across your app.


// Small button
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    textStyle: TextStyle(fontSize: 12),
  ),
  child: Text('Small'),
)

// Medium button (default)
ElevatedButton(
  onPressed: () {},
  child: Text('Medium'),
)

// Large button
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    padding: EdgeInsets.symmetric(horizontal: 48, vertical: 20),
    textStyle: TextStyle(fontSize: 18),
  ),
  child: Text('Large'),
)

Buttons with Icons

Adding icons to buttons makes them more intuitive and visually appealing. Flutter makes this easy with the Icon widget.


ElevatedButton.icon(
  onPressed: () {
    shareContent();
  },
  icon: Icon(Icons.share),
  label: Text('Share'),
  style: ElevatedButton.styleFrom(
    padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
  ),
)

TextButton.icon(
  onPressed: () {
    downloadFile();
  },
  icon: Icon(Icons.download),
  label: Text('Download'),
)

Notice the .icon constructor for buttons - it's a convenient way to add icons without manually wrapping children in a Row.

Creating Reusable Button Components

As your app grows, you'll want to create reusable button components that maintain consistency. Here's a pattern you can use:


class PrimaryButton extends StatelessWidget {
  final String text;
  final VoidCallback? onPressed;
  final bool isLoading;

  const PrimaryButton({
    Key? key,
    required this.text,
    this.onPressed,
    this.isLoading = false,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isLoading ? null : onPressed,
      style: ElevatedButton.styleFrom(
        backgroundColor: Theme.of(context).primaryColor,
        foregroundColor: Colors.white,
        padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
      child: isLoading
        ? SizedBox(
            width: 20,
            height: 20,
            child: CircularProgressIndicator(
              strokeWidth: 2,
              valueColor: AlwaysStoppedAnimation(Colors.white),
            ),
          )
        : Text(text),
    );
  }
}

Now you can use this button throughout your app:


PrimaryButton(
  text: 'Save Changes',
  onPressed: () {
    saveData();
  },
  isLoading: isSaving,
)

Accessibility Best Practices

Making your buttons accessible ensures all users can interact with your app effectively. Here are key practices:

  • Always provide tooltips for IconButton - Screen readers need descriptive text
  • Use semantic labels - "Save" is better than "Click here"
  • Ensure sufficient contrast - Text should be readable against button backgrounds
  • Provide feedback - Visual or haptic feedback confirms user actions
  • Size matters - Buttons should be at least 48x48 logical pixels for easy tapping

Semantics(
  button: true,
  label: 'Save document',
  child: ElevatedButton(
    onPressed: () {
      saveDocument();
    },
    child: Text('Save'),
  ),
)

Button Groups and Layouts

Often, you'll need to display multiple buttons together. Flutter's layout widgets make this straightforward.


// Horizontal button group
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    TextButton(
      onPressed: () => cancel(),
      child: Text('Cancel'),
    ),
    ElevatedButton(
      onPressed: () => confirm(),
      child: Text('Confirm'),
    ),
  ],
)

// Vertical button group
Column(
  children: [
    ElevatedButton(
      onPressed: () {},
      child: Text('Option 1'),
    ),
    SizedBox(height: 8),
    ElevatedButton(
      onPressed: () {},
      child: Text('Option 2'),
    ),
  ],
)

Common Patterns and Use Cases

Confirmation Dialogs


ElevatedButton(
  onPressed: () {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Delete Item'),
        content: Text('Are you sure you want to delete this item?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              deleteItem();
              Navigator.pop(context);
            },
            child: Text('Delete'),
          ),
        ],
      ),
    );
  },
  child: Text('Delete'),
)

Form Submission


Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Please enter your name';
          }
          return null;
        },
      ),
      SizedBox(height: 16),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            submitForm();
          }
        },
        child: Text('Submit'),
      ),
    ],
  ),
)

Performance Considerations

Buttons are lightweight widgets, but there are a few things to keep in mind for optimal performance:

  • Use const constructors when possible - This helps Flutter optimize rebuilds
  • Avoid rebuilding entire button trees - Extract button logic to separate widgets if needed
  • Don't create buttons in build methods unnecessarily - Cache button widgets if they don't change

// Good: const button when possible
const ElevatedButton(
  onPressed: null,
  child: Text('Disabled'),
)

// Good: Extract to separate widget for complex buttons
class SubmitButton extends StatelessWidget {
  final bool isLoading;
  final VoidCallback onPressed;

  const SubmitButton({
    Key? key,
    required this.isLoading,
    required this.onPressed,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isLoading ? null : onPressed,
      child: isLoading ? CircularProgressIndicator() : Text('Submit'),
    );
  }
}

Conclusion

Buttons are the bridge between your users and your app's functionality. By understanding Flutter's button widgets and their customization options, you can create interfaces that are both beautiful and functional. Remember to choose the right button type for each use case, maintain consistency across your app, and always prioritize accessibility.

Whether you're building a simple form or a complex dashboard, Flutter's button system gives you the flexibility to create exactly what your design requires. Start with the standard button types, customize them to match your brand, and create reusable components as your app grows. Happy button building!