Widgets

Button

Introduction

Nylo Website provides a Button class with eight pre-built button styles out of the box. Each button comes with built-in support for:

  • Async loading states — return a Future from onPressed and the button automatically shows a loading indicator
  • Animation styles — choose from clickable, bounce, pulse, squeeze, jelly, shine, ripple, morph, and shake effects
  • Splash styles — add ripple, highlight, glow, or ink touch feedback
  • Form submission — wire a button directly to a NyFormData instance

You can find your app's button definitions in lib/resources/widgets/buttons/buttons.dart. This file contains a Button class with static methods for each button type, making it easy to customize the defaults for your project.

Basic Usage

Use the Button class anywhere in your widgets. Here's a simple example inside a page:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            Button.primary(
              text: "Sign Up",
              onPressed: () {
                routeTo(SignUpPage.path);
              },
            ),

            SizedBox(height: 12),

            Button.secondary(
              text: "Learn More",
              onPressed: () {
                routeTo(AboutPage.path);
              },
            ),

            SizedBox(height: 12),

            Button.outlined(
              text: "Cancel",
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
    ),
  );
}

Every button type follows the same pattern — pass a text label and an onPressed callback.

Available Button Types

All buttons are accessed through the Button class using static methods.

Primary

A filled button with a shadow, using your theme's primary color. Best for main call-to-action elements.

Button.primary(
  text: "Sign Up",
  onPressed: () {
    // Handle press
  },
)

Secondary

A filled button with a softer surface color and subtle shadow. Good for secondary actions alongside a primary button.

Button.secondary(
  text: "Learn More",
  onPressed: () {
    // Handle press
  },
)

Outlined

A transparent button with a border stroke. Useful for less prominent actions or cancel buttons.

Button.outlined(
  text: "Cancel",
  onPressed: () {
    // Handle press
  },
)

You can customize the border and text colors:

Button.outlined(
  text: "Custom Outline",
  borderColor: Colors.red,
  textColor: Colors.red,
  onPressed: () {},
)

Text Only

A minimal button with no background or border. Ideal for inline actions or links.

Button.textOnly(
  text: "Skip",
  onPressed: () {
    // Handle press
  },
)

You can customize the text color:

Button.textOnly(
  text: "View Details",
  textColor: Colors.blue,
  onPressed: () {},
)

Icon

A filled button that displays an icon alongside the text. The icon appears before the text by default.

Button.icon(
  text: "Add to Cart",
  icon: Icon(Icons.shopping_cart),
  onPressed: () {
    // Handle press
  },
)

You can customize the background color:

Button.icon(
  text: "Download",
  icon: Icon(Icons.download),
  color: Colors.green,
  onPressed: () {},
)

Gradient

A button with a linear gradient background. Uses your theme's primary and tertiary colors by default.

Button.gradient(
  text: "Get Started",
  onPressed: () {
    // Handle press
  },
)

You can provide custom gradient colors:

Button.gradient(
  text: "Premium",
  gradientColors: [Colors.purple, Colors.pink],
  onPressed: () {},
)

Rounded

A pill-shaped button with fully rounded corners. The border radius defaults to half the button height.

Button.rounded(
  text: "Continue",
  onPressed: () {
    // Handle press
  },
)

You can customize the background color and border radius:

Button.rounded(
  text: "Apply",
  backgroundColor: Colors.teal,
  borderRadius: BorderRadius.circular(20),
  onPressed: () {},
)

Transparency

A frosted glass-style button with a backdrop blur effect. Works well when placed over images or colored backgrounds.

Button.transparency(
  text: "Explore",
  onPressed: () {
    // Handle press
  },
)

You can customize the text color:

Button.transparency(
  text: "View More",
  color: Colors.white,
  onPressed: () {},
)

Async Loading State

One of the most powerful features of Nylo Website buttons is automatic loading state management. When your onPressed callback returns a Future, the button will automatically display a loading indicator and disable interaction until the operation completes.

Button.primary(
  text: "Submit",
  onPressed: () async {
    await sleep(3); // Simulates a 3 second async task
  },
)

While the async operation is running, the button will show a skeleton loading effect (by default). Once the Future completes, the button returns to its normal state.

This works with any async operation — API calls, database writes, file uploads, or anything that returns a Future:

Button.primary(
  text: "Save Profile",
  onPressed: () async {
    await api<ApiService>((request) =>
      request.updateProfile(name: "John", email: "john@example.com")
    );
    showToastSuccess(description: "Profile saved!");
  },
)
Button.secondary(
  text: "Sync Data",
  onPressed: () async {
    await fetchAndStoreData();
    await clearOldCache();
  },
)

No need to manage isLoading state variables, call setState, or wrap anything in a StatefulWidget — Nylo Website handles it all for you.

How It Works

When the button detects that onPressed returns a Future, it uses the lockRelease mechanism to:

  1. Show a loading indicator (controlled by LoadingStyle)
  2. Disable the button to prevent duplicate taps
  3. Wait for the Future to complete
  4. Restore the button to its normal state

Animation Styles

Buttons support press animations through ButtonAnimationStyle. These animations provide visual feedback when a user interacts with a button. You can set the animation style when customizing your buttons in lib/resources/widgets/buttons/buttons.dart.

Clickable

A Duolingo-style 3D press effect. The button translates downward on press and springs back on release. Best for primary actions and game-like UX.

animationStyle: ButtonAnimationStyle.clickable()

Fine-tune the effect:

ButtonAnimationStyle.clickable(
  translateY: 6.0,        // How far the button moves down (default: 4.0)
  shadowOffset: 6.0,      // Shadow depth (default: 4.0)
  duration: Duration(milliseconds: 100),
  enableHapticFeedback: true,
)

Bounce

Scales the button down on press and springs back on release. Best for add-to-cart, like, and favorite buttons.

animationStyle: ButtonAnimationStyle.bounce()

Fine-tune the effect:

ButtonAnimationStyle.bounce(
  scaleMin: 0.90,         // Minimum scale on press (default: 0.92)
  duration: Duration(milliseconds: 150),
  curve: Curves.easeOutBack,
  enableHapticFeedback: true,
)

Pulse

A subtle continuous scale pulse while the button is held down. Best for long-press actions or drawing attention.

animationStyle: ButtonAnimationStyle.pulse()

Fine-tune the effect:

ButtonAnimationStyle.pulse(
  pulseScale: 1.08,       // Max scale during pulse (default: 1.05)
  duration: Duration(milliseconds: 800),
  curve: Curves.easeInOut,
)

Squeeze

Compresses the button horizontally and expands it vertically on press. Best for playful and interactive UIs.

animationStyle: ButtonAnimationStyle.squeeze()

Fine-tune the effect:

ButtonAnimationStyle.squeeze(
  squeezeX: 0.93,         // Horizontal scale (default: 0.95)
  squeezeY: 1.07,         // Vertical scale (default: 1.05)
  duration: Duration(milliseconds: 120),
  enableHapticFeedback: true,
)

Jelly

A wobbly elastic deformation effect. Best for fun, casual, or entertainment apps.

animationStyle: ButtonAnimationStyle.jelly()

Fine-tune the effect:

ButtonAnimationStyle.jelly(
  jellyStrength: 0.2,     // Wobble intensity (default: 0.15)
  duration: Duration(milliseconds: 300),
  curve: Curves.elasticOut,
  enableHapticFeedback: true,
)

Shine

A glossy highlight that sweeps across the button on press. Best for premium features or CTAs you want to draw attention to.

animationStyle: ButtonAnimationStyle.shine()

Fine-tune the effect:

ButtonAnimationStyle.shine(
  shineColor: Colors.white,  // Color of the shine streak (default: white)
  shineWidth: 0.4,           // Width of the shine band (default: 0.3)
  duration: Duration(milliseconds: 600),
)

Ripple

An enhanced ripple effect that expands from the touch point. Best for Material Design emphasis.

animationStyle: ButtonAnimationStyle.ripple()

Fine-tune the effect:

ButtonAnimationStyle.ripple(
  rippleScale: 2.5,       // How far the ripple expands (default: 2.0)
  duration: Duration(milliseconds: 400),
  curve: Curves.easeOut,
  enableHapticFeedback: true,
)

Morph

The button's border radius increases on press, creating a shape-shifting effect. Best for subtle, elegant feedback.

animationStyle: ButtonAnimationStyle.morph()

Fine-tune the effect:

ButtonAnimationStyle.morph(
  morphRadius: 30.0,      // Target border radius on press (default: 24.0)
  duration: Duration(milliseconds: 150),
  curve: Curves.easeInOut,
)

Shake

A horizontal shake animation. Best for error states or invalid actions — shake the button to signal something went wrong.

animationStyle: ButtonAnimationStyle.shake()

Fine-tune the effect:

ButtonAnimationStyle.shake(
  shakeOffset: 10.0,      // Horizontal displacement (default: 8.0)
  shakeCount: 4,          // Number of shakes (default: 3)
  duration: Duration(milliseconds: 400),
  enableHapticFeedback: true,
)

Disabling Animations

To use a button with no animation:

animationStyle: ButtonAnimationStyle.none()

Changing the Default Animation

To change the default animation for a button type, modify your lib/resources/widgets/buttons/buttons.dart file:

class Button {
  static Widget primary({
    required String text,
    VoidCallback? onPressed,
    ...
  }) {
    return PrimaryButton(
      text: text,
      onPressed: onPressed,
      animationStyle: ButtonAnimationStyle.bounce(), // Change the default
    );
  }
}

Splash Styles

Splash effects provide visual touch feedback on buttons. Configure them through ButtonSplashStyle. Splash styles can be combined with animation styles for layered feedback.

Available Splash Styles

Splash Factory Description
Ripple ButtonSplashStyle.ripple() Standard Material ripple from touch point
Highlight ButtonSplashStyle.highlight() Subtle highlight without ripple animation
Glow ButtonSplashStyle.glow() Soft glow radiating from touch point
Ink ButtonSplashStyle.ink() Quick ink splash, faster and more responsive
None ButtonSplashStyle.none() No splash effect
Custom ButtonSplashStyle.custom() Full control over the splash factory

Example

class Button {
  static Widget outlined({
    required String text,
    VoidCallback? onPressed,
    ...
  }) {
    return OutlinedButton(
      text: text,
      onPressed: onPressed,
      splashStyle: ButtonSplashStyle.ripple(),
      animationStyle: ButtonAnimationStyle.clickable(),
    );
  }
}

You can customize splash colors and opacity:

ButtonSplashStyle.ripple(
  splashColor: Colors.blue,
  highlightColor: Colors.blue,
  splashOpacity: 0.2,
  highlightOpacity: 0.1,
)

Loading Styles

The loading indicator shown during async operations is controlled by LoadingStyle. You can set it per button type in your buttons file.

Skeletonizer (Default)

Displays a shimmer skeleton effect over the button:

loadingStyle: LoadingStyle.skeletonizer()

Normal

Shows a loading widget (defaults to the app loader):

loadingStyle: LoadingStyle.normal(
  child: Text("Please wait..."),
)

None

Keeps the button visible but disables interaction during loading:

loadingStyle: LoadingStyle.none()

Form Submission

All buttons support the submitForm parameter, which connects the button to a NyForm. When tapped, the button will validate the form and call your success handler with the form data.

Button.primary(
  text: "Submit",
  submitForm: (LoginForm(), (data) {
    // data contains the validated form fields
    print(data);
  }),
  onFailure: (error) {
    // Handle validation errors
    print(error);
  },
)

The submitForm parameter accepts a record with two values:

  1. A NyFormData instance (or form name as a String)
  2. A callback that receives the validated data

By default, showToastError is true, which displays a toast notification when form validation fails. Set it to false to handle errors silently:

Button.primary(
  text: "Login",
  submitForm: (LoginForm(), (data) async {
    await api<AuthApiService>((request) => request.login(data));
  }),
  showToastError: false,
  onFailure: (error) {
    // Custom error handling
  },
)

When the submitForm callback returns a Future, the button will automatically show a loading state until the async operation completes.

Customizing Buttons

All button defaults are defined in your project at lib/resources/widgets/buttons/buttons.dart. Each button type has a corresponding widget class in lib/resources/widgets/buttons/partials/.

Changing Default Styles

To modify a button's default appearance, edit the Button class:

class Button {
  static Widget primary({
    required String text,
    VoidCallback? onPressed,
    Function(dynamic error)? onFailure,
    bool showToastError = true,
    double? width,
  }) {
    return PrimaryButton(
      text: text,
      onPressed: onPressed,
      submitForm: submitForm,
      onFailure: onFailure,
      showToastError: showToastError,
      loadingStyle: LoadingStyle.skeletonizer(),
      width: width,
      height: 52.0,
      animationStyle: ButtonAnimationStyle.bounce(),
      splashStyle: ButtonSplashStyle.glow(),
    );
  }
}

Customizing a Button Widget

To change the visual appearance of a button type, edit the corresponding widget in lib/resources/widgets/buttons/partials/. For example, to change the primary button's border radius or shadow:

// lib/resources/widgets/buttons/partials/primary_button_widget.dart

class PrimaryButton extends StatefulAppButton {
  ...

  @override
  Widget buildButton(BuildContext context) {
    final theme = Theme.of(context);
    final bgColor = backgroundColor ?? theme.colorScheme.primary;
    final fgColor = contentColor ?? theme.colorScheme.onPrimary;

    return Container(
      width: width ?? double.infinity,
      height: height,
      decoration: BoxDecoration(
        color: bgColor,
        borderRadius: BorderRadius.circular(8), // Change the radius
      ),
      child: Center(
        child: Text(
          text,
          style: TextStyle(
            color: fgColor,
            fontSize: 16,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    );
  }
}

Creating a New Button Type

To add a new button type:

  1. Create a new widget file in lib/resources/widgets/buttons/partials/ extending StatefulAppButton.
  2. Implement the buildButton method.
  3. Add a static method in the Button class.
// lib/resources/widgets/buttons/partials/danger_button_widget.dart

class DangerButton extends StatefulAppButton {
  DangerButton({
    required super.text,
    super.onPressed,
    super.submitForm,
    super.onFailure,
    super.showToastError,
    super.loadingStyle,
    super.width,
    super.height,
    super.animationStyle,
    super.splashStyle,
  });

  @override
  Widget buildButton(BuildContext context) {
    return Container(
      width: width ?? double.infinity,
      height: height,
      decoration: BoxDecoration(
        color: Colors.red,
        borderRadius: BorderRadius.circular(14),
      ),
      child: Center(
        child: Text(
          text,
          style: TextStyle(
            color: Colors.white,
            fontSize: 16,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    );
  }
}

Then register it in the Button class:

class Button {
  ...

  static Widget danger({
    required String text,
    VoidCallback? onPressed,
    Function(dynamic error)? onFailure,
    bool showToastError = true,
    double? width,
  }) {
    return DangerButton(
      text: text,
      onPressed: onPressed,
      submitForm: submitForm,
      onFailure: onFailure,
      showToastError: showToastError,
      loadingStyle: LoadingStyle.skeletonizer(),
      width: width,
      height: 52.0,
      animationStyle: ButtonAnimationStyle.shake(),
    );
  }
}

Parameters Reference

Common Parameters (All Button Types)

Parameter Type Default Description
text String required The button label text
onPressed VoidCallback? null Callback when the button is tapped. Return a Future for automatic loading state
submitForm (dynamic, Function(dynamic))? null Form submission record (form instance, success callback)
onFailure Function(dynamic)? null Called when form validation fails
showToastError bool true Show toast notification on form validation error
width double? null Button width (defaults to full width)

Type-Specific Parameters

Button.outlined

Parameter Type Default Description
borderColor Color? Theme outline color Border stroke color
textColor Color? Theme primary color Text color

Button.textOnly

Parameter Type Default Description
textColor Color? Theme primary color Text color

Button.icon

Parameter Type Default Description
icon Widget required The icon widget to display
color Color? Theme primary color Background color

Button.gradient

Parameter Type Default Description
gradientColors List<Color>? Primary and tertiary colors Gradient color stops

Button.rounded

Parameter Type Default Description
backgroundColor Color? Theme primary container color Background color
borderRadius BorderRadius? Pill shape (height / 2) Corner radius

Button.transparency

Parameter Type Default Description
color Color? Theme-adaptive Text color

ButtonAnimationStyle Parameters

Parameter Type Default Description
duration Duration Varies per type Animation duration
curve Curve Varies per type Animation curve
enableHapticFeedback bool Varies per type Trigger haptic feedback on press
translateY double 4.0 Clickable: vertical press distance
shadowOffset double 4.0 Clickable: shadow depth
scaleMin double 0.92 Bounce: minimum scale on press
pulseScale double 1.05 Pulse: maximum scale during pulse
squeezeX double 0.95 Squeeze: horizontal compression
squeezeY double 1.05 Squeeze: vertical expansion
jellyStrength double 0.15 Jelly: wobble intensity
shineColor Color Colors.white Shine: highlight color
shineWidth double 0.3 Shine: width of the shine band
rippleScale double 2.0 Ripple: expansion scale
morphRadius double 24.0 Morph: target border radius
shakeOffset double 8.0 Shake: horizontal displacement
shakeCount int 3 Shake: number of oscillations

ButtonSplashStyle Parameters

Parameter Type Default Description
splashColor Color? Theme surface color Splash effect color
highlightColor Color? Theme surface color Highlight effect color
splashOpacity double 0.12 Opacity of the splash
highlightOpacity double 0.06 Opacity of the highlight
borderRadius BorderRadius? null Splash clip radius