In this article, we'll walk you through how to build a custom button widget in Flutter that looks modern, stylish, and functional — similar to what you would see in fitness apps showing total calories burned.
We’ll add:
✅ Unique Arrow Clip Shape
✅ Smooth Rounded Corners
✅ Optional Gradient Background
✅ Proper Ripple Effect on tap
✅ Customizable content (title, icon, color, radius, etc).
Flutter provides great built-in buttons like ElevatedButton
and TextButton
, but sometimes you need a more visually appealing, app-specific component to match your design language.
This is exactly what we’ll achieve.
🛠️ How to Implement
1. Create an Arrow Clipper
We need to clip the left side of the button into an arrow shape:
class ArrowClipper extends CustomClipper<Path> { @override Path getClip(Size size) { double radius = 16; final path = Path(); path.moveTo(0, radius); path.quadraticBezierTo(0, 0, radius, 0); path.lineTo(size.width * 0.80, 0); path.lineTo(size.width, size.height / 2); path.lineTo(size.width * 0.80, size.height); path.lineTo(radius, size.height); path.quadraticBezierTo(0, size.height, 0, size.height - radius); path.close(); return path; } @override bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false; }
2. Build the Custom Button Widget
Here’s the full reusable CalorieButton
widget:
import 'package:flutter/material.dart'; class CalorieButton extends StatelessWidget { final double value; final VoidCallback? onTap; final double borderRadius; final List<Color> gradientColors; final Color solidColor; final String title; final String? description; final IconData icon; final double height; final double width; const CalorieButton({ super.key, required this.value, required this.onTap, this.borderRadius = 16, required this.gradientColors, this.solidColor = Colors.red, this.title = 'Total Calories', this.description = 'kCal', this.icon = LucideIcons.flame, this.height = 70, this.width = 250, }); @override Widget build(BuildContext context) { return Material( color: Colors.white, elevation: 4, borderRadius: BorderRadius.circular(borderRadius), clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, splashColor: gradientColors[0].withOpacity(0.2), child: SizedBox( height: height, width: width, child: Row( children: [ // Left arrow shape ClipPath( clipper: ArrowClipper(), child: Container( width: 80, decoration: BoxDecoration( gradient: LinearGradient( colors: gradientColors!, begin: Alignment.topLeft, end: Alignment.bottomRight, ), //color: gradientColors == null ? solidColor : null, ), child: Center( child: Icon( icon, color: Colors.white, size: 28, ), ), ), ), const SizedBox(width: 16), // Text content Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( color: Colors.grey[600], fontSize: 14, ), ), const SizedBox(height: 4), Text( '$value', style: TextStyle( color: gradientColors[0], fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 2), Text( description ?? '', style: const TextStyle( color: Colors.grey, fontSize: 12, ), ), ], ), ), const Padding( padding: EdgeInsets.only(right: 16.0), child: Icon( Icons.chevron_right, color: Colors.grey, size: 24, ), ), ], ), ), ), ); } }
3. Example Usage:
CalorieButton( value: 4800, title: 'Total Steps', description: 'Steps', icon: LucideIcons.footprints, onTap: () {}, gradientColors: const [ Colors.red, Color(0xFFcd2828), Color(0xFF982121), Color(0xFF761313), ], ),