Skip to main content
← back

app button

lib/app_button.dart · flutter · dart

a button for flutter in one file. three types, three sizes, spring press feedback. no packages, no DI, no widget wrapping.

llms.txt

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, fill, gradient and halo all land together.

live demo

starting the demo

install

copy one file

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

app_button.dart724 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.