Star Rating
Tap-to-rate star bar — lavender filled stars, half-star display, sizes & read-only
View component →A single, reusable Flutter quantity stepper — a minus button, the current count, and a plus button inside one hairline-bordered control. It clamps to a min and max, dimming and disabling the minus at the floor and the plus at the ceiling, with 44px touch targets on each button. No external packages, fully DartPad-ready.
What's Included
Use Cases
PRO TIP
Always clamp to a real min and max and disable the button at each end rather than letting the value run negative or unbounded — a cart quantity that drops below one or a booking past capacity is a bug waiting to happen. Drive the count from your own state inside onChanged, and keep each button at least 44px so it stays tappable on a crowded list row.
Copy this into your Flutter project. No external packages required.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _qty = 1;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: const Color(0xFFFAFAFB),
body: Center(
child: AppQuantitySelector(
value: _qty,
min: 1,
max: 10,
onChanged: (v) => setState(() => _qty = v),
),
),
),
);
}
}
/// FlutterKit Quantity Selector — house design system (Linear lavender, light).
/// Minus / value / plus stepper, clamped to a min and max.
/// rohansurve.in/flutterkit/quantity-selector
class AppQuantitySelector extends StatelessWidget {
const AppQuantitySelector({
super.key,
required this.value,
this.onChanged,
this.min = 0,
this.max = 99,
this.enabled = true,
});
/// Current count.
final int value;
/// Called with the next count on tap.
final ValueChanged<int>? onChanged;
/// Lower and upper bounds.
final int min;
final int max;
/// Disabled look — both buttons dim, taps ignored.
final bool enabled;
// ── House tokens — docs/flutterkit-design.md §1 ──
static const Color _hairlineStrong = Color(0xFFD5D8DD);
static const Color _ink = Color(0xFF1C1E26);
static const Color _inkTertiary = Color(0xFFB4B8BF);
@override
Widget build(BuildContext context) {
final canDec = enabled && value > min;
final canInc = enabled && value < max;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _hairlineStrong),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_btn(Icons.remove, canDec, () => onChanged!(value - 1)),
Container(
constraints: const BoxConstraints(minWidth: 44),
height: 44,
alignment: Alignment.center,
decoration: const BoxDecoration(
border: Border(
left: BorderSide(color: _hairlineStrong),
right: BorderSide(color: _hairlineStrong),
),
),
child: Text(
value.toString(),
style: const TextStyle(
color: _ink,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
_btn(Icons.add, canInc, () => onChanged!(value + 1)),
],
),
);
}
Widget _btn(IconData icon, bool active, VoidCallback onTap) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: active ? onTap : null,
child: SizedBox(
width: 44,
height: 44,
child: Icon(icon, size: 18, color: active ? _ink : _inkTertiary),
),
),
);
}
}
// ── Usage ──
// AppQuantitySelector(value: v, onChanged: (n) {}) // default
// AppQuantitySelector(value: v, min: 1, max: 10, onChanged: (n){}) // bounded
// const AppQuantitySelector(value: 1, enabled: false) // disabledTap-to-rate star bar — lavender filled stars, half-star display, sizes & read-only
View component →One enum-driven button — 5 types, solid/pill/outline styles, 4 sizes, icon + loading
View component →iOS-style segmented control — sliding white selection on a muted track, icons & disabled
View component →