Skip to content
HyperUI for Flutter requires the Style package — this site itself ships no pub package.

Quantity Inputs

Ported from HyperUI’s Quantity Inputs — see Attribution.

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('+'),
),
),
],
),
],
);
}
}

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('+'),
),
),
],
),
],
);
}
}

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('+'),
),
),
],
),
],
);
}
}

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('+'),
),
),
],
),
],
);
}
}