Skip to main content

tldr

copy one file into your project, use it. no setup, no init call.

AppButton(text: "Continue", onPressed: _submit).

three types, three sizes, loading and disabled states, leading/trailing icons. press feedback runs on a spring, so scale and fill land together.

live demo

starting the demo

install

copy one file

grab it and drop it in at lib/app_button.dart.

app_button.dart676 lines

it imports material and physics from the flutter sdk. nothing goes in pubspec.yaml.

use it

there is no init call. the defaults stand on their own.

AppButton(text: "Continue", onPressed: _submit)

usage

three types. one primary per screen, that's the point of it.

AppButton(text: "Continue", onPressed: _submit);
AppButton(text: "Cancel", type: AppButtonType.secondary, onPressed: _close);
AppButton(text: "Skip", type: AppButtonType.tertiary, onPressed: _skip);

three sizes. lg is the default, since most buttons in an app sit at the bottom of a screen.

AppButton(text: "Save", size: AppButtonSize.sm, onPressed: _save);
AppButton(text: "Save", size: AppButtonSize.md, onPressed: _save);
AppButton(text: "Save", size: AppButtonSize.lg, onPressed: _save);

loading swaps the content for a spinner and blocks taps. disabled does the same without the spinner.

AppButton(text: "Saving", isLoading: true, onPressed: _save);
AppButton(text: "Save", isDisabled: true, onPressed: _save);
AppButton(text: "Save", onPressed: null); // also disabled

icons on either side. text is optional if you pass one.

AppButton(
  text: "Continue",
  trailingIcon: const Icon(Icons.arrow_forward, size: 18),
  onPressed: _next,
);

AppButton(
  leadingIcon: const Icon(Icons.close, size: 18),
  semanticLabel: "Close",
  type: AppButtonType.tertiary,
  onPressed: _close,
);

stretch it across the screen.

AppButton(text: "Continue", width: double.infinity, onPressed: _submit);

example

class CheckoutView extends StatefulWidget {
  const CheckoutView({super.key});

  @override
  State<CheckoutView> createState() => _CheckoutViewState();
}

class _CheckoutViewState extends State<CheckoutView> {
  bool _isPaying = false;

  Future<void> _pay() async {
    setState(() => _isPaying = true);
    await context.read<Cart>().checkout();
    if (mounted) setState(() => _isPaying = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: const CartList(),
      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              AppButton(
                text: "Pay now",
                width: double.infinity,
                isLoading: _isPaying,
                onPressed: _pay,
              ),
              const SizedBox(height: 8),
              AppButton(
                text: "Keep shopping",
                type: AppButtonType.tertiary,
                size: AppButtonSize.md,
                onPressed: () => Navigator.pop(context),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

reference

everything below is here when you need it. you can ship without reading any of it.