SELECTION CONTROLS

Quantity Selector

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.

stepperquantitycounter
Preview5 variants

Default

3

Higher count

12

What's Included

  • Minus / value / plus laid out in one bordered control
  • Clamps to a configurable min and max
  • Disabled minus at the floor, disabled plus at the ceiling
  • 44px touch targets on each button
  • Hairline dividers separating the value from the buttons
  • One AppQuantitySelector driven by an int + onChanged

Use Cases

  • Cart line items — adjust product quantity
  • Booking guests, tickets, or room counts
  • Numeric form fields with sensible bounds
  • Anywhere a small integer is incremented or decremented

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.

Complete Dart Code

Copy this into your Flutter project. No external packages required.

dart
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)              // disabled

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — app_quantity_selector.dart in your Flutter project
  3. 3Use AppQuantitySelector(value: v, min: 1, max: 10, onChanged: (n) {}) — switch the look with the min/max and enabled parameters

Related components

Selection Controls

Star Rating

Tap-to-rate star bar — lavender filled stars, half-star display, sizes & read-only

View component →
Buttons & Actions

Button Widget

One enum-driven button — 5 types, solid/pill/outline styles, 4 sizes, icon + loading

View component →
Selection Controls

Segmented Control

iOS-style segmented control — sliding white selection on a muted track, icons & disabled

View component →