Quantity Inputs
Ported from HyperUI’s Quantity Inputs — see Attribution.
Base with plus and minus buttons
Section titled “Base with plus and minus buttons”Includes a dark: variant — toggle the site theme (top right) to preview it.
class QuantityInputs1Example extends StatefulWidget { const QuantityInputs1Example({super.key});
@override State<QuantityInputs1Example> createState() => _QuantityInputs1ExampleState();}
class _QuantityInputs1ExampleState extends State<QuantityInputs1Example> { int _quantity = 1; final _controller = TextEditingController(text: '1');
@override void dispose() { _controller.dispose(); super.dispose(); }
void _setQuantity(int next) { setState(() { _quantity = next; _controller.text = '$_quantity'; }); }
@override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSmUp = MediaQuery.sizeOf(context).width >= 640; final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle();
return Style( 'flex justify-center p-6', children: [ const Style('sr-only', child: Text('Quantity')), Style( 'flex items-center gap-1', children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity - 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('−'), ), ), Style( 'h-10 w-24 rounded-sm border-gray-200 sm:text-sm dark:border-gray-700 dark:bg-gray-800 dark:text-white', child: TextFormField( controller: _controller, keyboardType: TextInputType.number, onChanged: (text) { final parsed = int.tryParse(text); if (parsed != null) _quantity = parsed; }, style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity + 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('+'), ), ), ], ), ], ); }}<div> <label for="Quantity" class="sr-only"> Quantity </label>
<div class="flex items-center gap-1"> <button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> − </button>
<input type="number" id="Quantity" value="1" class="h-10 w-24 rounded-sm border-gray-200 sm:text-sm" />
<button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> + </button> </div></div>Base with plus and minus buttons, no spinners in the input
Section titled “Base with plus and minus buttons, no spinners in the input”Includes a dark: variant — toggle the site theme (top right) to preview it.
class QuantityInputs2Example extends StatefulWidget { const QuantityInputs2Example({super.key});
@override State<QuantityInputs2Example> createState() => _QuantityInputs2ExampleState();}
class _QuantityInputs2ExampleState extends State<QuantityInputs2Example> { int _quantity = 1; final _controller = TextEditingController(text: '1');
@override void dispose() { _controller.dispose(); super.dispose(); }
void _setQuantity(int next) { setState(() { _quantity = next; _controller.text = '$_quantity'; }); }
@override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSmUp = MediaQuery.sizeOf(context).width >= 640; final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle();
return Style( 'flex justify-center p-6', children: [ const Style('sr-only', child: Text('Quantity')), Style( 'flex items-center gap-1', children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity - 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('−'), ), ), Style( 'h-10 w-24 rounded-sm border-gray-200 [-moz-appearance:textfield] sm:text-sm dark:border-gray-700 dark:bg-gray-800 dark:text-white [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none', child: TextFormField( controller: _controller, keyboardType: TextInputType.number, onChanged: (text) { final parsed = int.tryParse(text); if (parsed != null) _quantity = parsed; }, style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity + 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('+'), ), ), ], ), ], ); }}<div> <label for="Quantity" class="sr-only"> Quantity </label>
<div class="flex items-center gap-1"> <button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> − </button>
<input type="number" id="Quantity" value="1" class="h-10 w-24 rounded-sm border-gray-200 [-moz-appearance:textfield] sm:text-sm [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none" />
<button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> + </button> </div></div>Base with plus and minus buttons, no spinners in the input and text centered
Section titled “Base with plus and minus buttons, no spinners in the input and text centered”Includes a dark: variant — toggle the site theme (top right) to preview it.
class QuantityInputs3Example extends StatefulWidget { const QuantityInputs3Example({super.key});
@override State<QuantityInputs3Example> createState() => _QuantityInputs3ExampleState();}
class _QuantityInputs3ExampleState extends State<QuantityInputs3Example> { int _quantity = 1; final _controller = TextEditingController(text: '1');
@override void dispose() { _controller.dispose(); super.dispose(); }
void _setQuantity(int next) { setState(() { _quantity = next; _controller.text = '$_quantity'; }); }
@override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSmUp = MediaQuery.sizeOf(context).width >= 640; final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle();
return Style( 'flex justify-center p-6', children: [ const Style('sr-only', child: Text('Quantity')), Style( 'flex items-center gap-1', children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity - 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('−'), ), ), Style( 'h-10 w-16 rounded-sm border-gray-200 text-center [-moz-appearance:textfield] sm:text-sm dark:border-gray-700 dark:bg-gray-800 dark:text-white [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none', child: TextFormField( controller: _controller, keyboardType: TextInputType.number, textAlign: TextAlign.center, onChanged: (text) { final parsed = int.tryParse(text); if (parsed != null) _quantity = parsed; }, style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity + 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('+'), ), ), ], ), ], ); }}<div> <label for="Quantity" class="sr-only"> Quantity </label>
<div class="flex items-center gap-1"> <button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> − </button>
<input type="number" id="Quantity" value="1" class="h-10 w-16 rounded-sm border-gray-200 text-center [-moz-appearance:textfield] sm:text-sm [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none" />
<button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> + </button> </div></div>Bordered with plus and minus buttons
Section titled “Bordered with plus and minus buttons”Includes a dark: variant — toggle the site theme (top right) to preview it.
class QuantityInputs4Example extends StatefulWidget { const QuantityInputs4Example({super.key});
@override State<QuantityInputs4Example> createState() => _QuantityInputs4ExampleState();}
class _QuantityInputs4ExampleState extends State<QuantityInputs4Example> { int _quantity = 1; final _controller = TextEditingController(text: '1');
@override void dispose() { _controller.dispose(); super.dispose(); }
void _setQuantity(int next) { setState(() { _quantity = next; _controller.text = '$_quantity'; }); }
@override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final isSmUp = MediaQuery.sizeOf(context).width >= 640; final fontSize = isSmUp ? 14.0 : 16.0; final lineHeight = isSmUp ? 20.0 : 24.0; final baseStyle = Theme.of(context).textTheme.bodyLarge ?? const TextStyle();
return Style( 'flex justify-center p-6', children: [ const Style('sr-only', child: Text('Quantity')), Style( 'flex items-center rounded-sm border border-gray-200 dark:border-gray-800', children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity - 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('−'), ), ), Style( 'h-10 w-16 border-transparent text-center [-moz-appearance:textfield] sm:text-sm dark:bg-gray-900 dark:text-white [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none', child: TextFormField( controller: _controller, keyboardType: TextInputType.number, textAlign: TextAlign.center, onChanged: (text) { final parsed = int.tryParse(text); if (parsed != null) _quantity = parsed; }, style: baseStyle.copyWith( fontSize: fontSize, height: lineHeight / fontSize, color: isDark ? Colors.white : Colors.black, ), ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _setQuantity(_quantity + 1), child: Style( 'size-10 leading-10 text-gray-600 transition hover:opacity-75 dark:text-gray-300', button: true, child: const Text('+'), ), ), ], ), ], ); }}<div> <label for="Quantity" class="sr-only"> Quantity </label>
<div class="flex items-center rounded-sm border border-gray-200"> <button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> − </button>
<input type="number" id="Quantity" value="1" class="h-10 w-16 border-transparent text-center [-moz-appearance:textfield] sm:text-sm [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none" />
<button type="button" class="size-10 leading-10 text-gray-600 transition hover:opacity-75"> + </button> </div></div>