BUTTONS & ACTIONS

Button Widget

A single, reusable Flutter button widget driven entirely by enums. Switch between 5 types (Primary, Secondary, Success, Danger, Warning), 3 styles (Solid, Pill, Outline), and 4 sizes with one parameter each — plus built-in leading icon and loading spinner support. No external packages, fully DartPad-ready.

buttonactioncta
Preview15 variants
Default
Secondary
Success
Danger
Warning

What's Included

  • Solid buttons — Primary, Secondary, Success, Danger, Warning
  • Pill buttons — fully rounded variant for any type
  • Outline buttons — transparent fill with colored border and text
  • Four sizes — Extra Small, Default, Large, Extra Large
  • Leading icon support via an optional IconData parameter
  • Loading state with an inline CircularProgressIndicator and disabled tap

Use Cases

  • Primary call-to-action buttons — Buy now, Continue, Save
  • Destructive confirmations — Delete or Logout with the Danger type
  • Secondary actions — Cancel or Back paired beside a primary button
  • Async submit buttons — show the loading spinner during API calls

PRO TIP

Keep a single AppButton in your design system instead of scattering ElevatedButton, OutlinedButton, and TextButton with ad-hoc styles. Drive every look through the type, variant, and size enums — when your brand color changes, you edit one constant and every button in the app updates. Always pass isLoading during async work so users cannot double-tap and fire a request twice.

Complete Dart Code

Copy this into your Flutter project. No external packages required.

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.white,
        body: Center(
          child: AppButton(
            label: 'Buy now',
            icon: Icons.shopping_cart_outlined,
            onPressed: () {},
          ),
        ),
      ),
    );
  }
}

/// Flutter Button Widget — All Variants
/// One enum-driven button: 5 types, 3 styles, 4 sizes, icon + loading.
/// rohansurve.in/flutterkit/button-widget

enum ButtonType { primary, secondary, success, danger, warning }
enum ButtonVariant { solid, pill, outline }
enum ButtonSize { xs, sm, lg, xl }

class AppButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;
  final ButtonType type;
  final ButtonVariant variant;
  final ButtonSize size;
  final IconData? icon;
  final bool isLoading;

  const AppButton({
    super.key,
    required this.label,
    this.onPressed,
    this.type = ButtonType.primary,
    this.variant = ButtonVariant.solid,
    this.size = ButtonSize.sm,
    this.icon,
    this.isLoading = false,
  });

  static const Color _brand = Color(0xFF1D9E75);
  static const Color _success = Color(0xFF22C55E);
  static const Color _danger = Color(0xFFEF4444);
  static const Color _warning = Color(0xFFF59E0B);
  static const Color _secondaryBg = Color(0xFFF1F5F9);
  static const Color _secondaryText = Color(0xFF334155);
  static const Color _secondaryBorder = Color(0xFFE2E8F0);

  Color get _baseColor {
    switch (type) {
      case ButtonType.primary:
        return _brand;
      case ButtonType.secondary:
        return _secondaryBg;
      case ButtonType.success:
        return _success;
      case ButtonType.danger:
        return _danger;
      case ButtonType.warning:
        return _warning;
    }
  }

  double get _fontSize {
    switch (size) {
      case ButtonSize.xs:
        return 12;
      case ButtonSize.sm:
        return 14;
      case ButtonSize.lg:
        return 16;
      case ButtonSize.xl:
        return 20;
    }
  }

  EdgeInsets get _padding {
    switch (size) {
      case ButtonSize.xs:
        return const EdgeInsets.symmetric(horizontal: 12, vertical: 8);
      case ButtonSize.sm:
        return const EdgeInsets.symmetric(horizontal: 16, vertical: 10);
      case ButtonSize.lg:
        return const EdgeInsets.symmetric(horizontal: 20, vertical: 12);
      case ButtonSize.xl:
        return const EdgeInsets.symmetric(horizontal: 24, vertical: 14);
    }
  }

  @override
  Widget build(BuildContext context) {
    final bool isOutline = variant == ButtonVariant.outline;
    final bool isSecondary = type == ButtonType.secondary;
    final double radius = variant == ButtonVariant.pill ? 999 : 8;

    final Color bg = isOutline ? Colors.transparent : _baseColor;
    final Color fg = isOutline
        ? _baseColor
        : isSecondary
            ? _secondaryText
            : Colors.white;
    final Color borderColor = isOutline
        ? _baseColor
        : isSecondary
            ? _secondaryBorder
            : Colors.transparent;

    return Opacity(
      opacity: isLoading ? 0.75 : 1,
      child: Material(
        color: bg,
        borderRadius: BorderRadius.circular(radius),
        child: InkWell(
          onTap: isLoading ? null : onPressed,
          borderRadius: BorderRadius.circular(radius),
          child: Container(
            padding: _padding,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(radius),
              border: Border.all(color: borderColor),
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (isLoading) ...[
                  SizedBox(
                    width: _fontSize,
                    height: _fontSize,
                    child: CircularProgressIndicator(strokeWidth: 2, color: fg),
                  ),
                  const SizedBox(width: 8),
                ] else if (icon != null) ...[
                  Icon(icon, size: _fontSize + 2, color: fg),
                  const SizedBox(width: 8),
                ],
                Text(
                  label,
                  style: TextStyle(
                    color: fg,
                    fontSize: _fontSize,
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

// ─── USAGE EXAMPLES ──────────────────────────────────────────────

// Primary solid (default)
// AppButton(label: 'Default', onPressed: () {})

// Secondary solid
// AppButton(label: 'Secondary', type: ButtonType.secondary, onPressed: () {})

// Danger pill
// AppButton(label: 'Delete', type: ButtonType.danger, variant: ButtonVariant.pill, onPressed: () {})

// Success outline
// AppButton(label: 'Approve', type: ButtonType.success, variant: ButtonVariant.outline, onPressed: () {})

// Extra large with icon
// AppButton(label: 'Buy now', size: ButtonSize.xl, icon: Icons.shopping_cart_outlined, onPressed: () {})

// Loading state
// AppButton(label: 'Loading...', isLoading: true, onPressed: () {})

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — app_button.dart in your Flutter project
  3. 3Use AppButton(label: 'Buy now', onPressed: () {}) — switch the look with the type, variant, and size parameters

Related components

Feedback & Alerts

Alert Widget

Dismissible alert banners in 5 styles — Info, Success, Warning, and Danger

View component →
Auth & Onboarding

Login Screen

Clean email + password login with form validation and loading state

View component →
Data Display

Stat Card

Dashboard metric cards in 4 styles — Simple, Icon, Trend, and Progress

View component →