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

Range Inputs

Ported from HyperUI’s Range Inputs — see Attribution.

class RangeInputs1Example extends StatefulWidget {
const RangeInputs1Example({super.key});
@override
State<RangeInputs1Example> createState() => _RangeInputs1ExampleState();
}
class _RangeInputs1ExampleState extends State<RangeInputs1Example> {
static const _min = 0.0;
static const _max = 100.0;
double _value = 20;
void _updateFromDx(double dx, double width) {
if (width <= 0) return;
var fraction = (dx / width).clamp(0.0, 1.0);
if (Directionality.of(context) == TextDirection.rtl) {
fraction = 1 - fraction;
}
setState(() {
_value = (_min + fraction * (_max - _min)).roundToDouble();
});
}
@override
Widget build(BuildContext context) {
return Style(
'mx-auto max-w-sm p-6',
children: [
const Style(
'block text-sm font-medium text-gray-900',
child: Text('Max Volume'),
),
LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
onHorizontalDragUpdate: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
child: Style(
'mt-3 h-3.5 w-full appearance-none rounded-full bg-gray-300 [&::-webkit-slider-thumb]:size-7 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[6px] [&::-webkit-slider-thumb]:border-gray-500 [&::-webkit-slider-thumb]:bg-gray-200',
cursor: SystemMouseCursors.click,
value: _value,
min: _min,
max: _max,
),
);
},
),
],
);
}
}
class RangeInputs2Example extends StatefulWidget {
const RangeInputs2Example({super.key});
@override
State<RangeInputs2Example> createState() => _RangeInputs2ExampleState();
}
class _RangeInputs2ExampleState extends State<RangeInputs2Example> {
static const _min = 0.0;
static const _max = 100.0;
double _value = 20;
void _updateFromDx(double dx, double width) {
if (width <= 0) return;
var fraction = (dx / width).clamp(0.0, 1.0);
if (Directionality.of(context) == TextDirection.rtl) {
fraction = 1 - fraction;
}
setState(() {
_value = (_min + fraction * (_max - _min)).roundToDouble();
});
}
@override
Widget build(BuildContext context) {
return Style(
'mx-auto max-w-sm p-6',
children: [
const Style(
'block text-sm font-medium text-gray-900',
child: Text('Max Volume'),
),
Style(
'mt-3 flex items-center gap-3',
children: [
LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => _updateFromDx(
details.localPosition.dx,
constraints.maxWidth,
),
onHorizontalDragUpdate: (details) => _updateFromDx(
details.localPosition.dx,
constraints.maxWidth,
),
child: Style(
'h-3.5 w-full appearance-none rounded-full bg-gray-300 [&::-webkit-slider-thumb]:size-7 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[6px] [&::-webkit-slider-thumb]:border-gray-500 [&::-webkit-slider-thumb]:bg-gray-200',
cursor: SystemMouseCursors.click,
value: _value,
min: _min,
max: _max,
),
);
},
),
Style(
'text-sm/none font-medium text-gray-700',
child: Text('${_value.round()}%'),
),
],
),
],
);
}
}
class RangeInputs3Example extends StatefulWidget {
const RangeInputs3Example({super.key});
@override
State<RangeInputs3Example> createState() => _RangeInputs3ExampleState();
}
class _RangeInputs3ExampleState extends State<RangeInputs3Example> {
static const _min = 0.0;
static const _max = 100.0;
double _value = 20;
void _updateFromDx(double dx, double width) {
if (width <= 0) return;
var fraction = (dx / width).clamp(0.0, 1.0);
if (Directionality.of(context) == TextDirection.rtl) {
fraction = 1 - fraction;
}
setState(() {
_value = (_min + fraction * (_max - _min)).roundToDouble();
});
}
@override
Widget build(BuildContext context) {
return Style(
'mx-auto max-w-sm p-6',
children: [
const Style(
'block text-sm font-medium text-gray-900',
child: Text('Max Volume'),
),
LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
onHorizontalDragUpdate: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
child: Style(
'mt-3 h-3.5 w-full appearance-none rounded-full bg-gray-300 [&::-webkit-slider-thumb]:size-7 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[6px] [&::-webkit-slider-thumb]:border-gray-500 [&::-webkit-slider-thumb]:bg-gray-200',
cursor: SystemMouseCursors.click,
value: _value,
min: _min,
max: _max,
),
);
},
),
Style(
'mt-1 flex items-center justify-between',
children: [
const Style('text-xs font-medium text-gray-700', child: Text('0%')),
const Style(
'text-xs font-medium text-gray-700',
child: Text('100%'),
),
],
),
],
);
}
}
class RangeInputs4Example extends StatefulWidget {
const RangeInputs4Example({super.key});
@override
State<RangeInputs4Example> createState() => _RangeInputs4ExampleState();
}
class _RangeInputs4ExampleState extends State<RangeInputs4Example> {
static const _min = 0.0;
static const _max = 100.0;
static const _thumbDiameter = 14.0;
static const _trackHeight = 8.0;
static const _trackColorUnfilled = Color(0xFFEFEFEF);
static const _trackColorFilled = Color(0xFF0075FF);
static const _tickColor = Color(0xFF000000);
static const _ticks = [0, 25, 50, 75, 100];
static const _trackTopInset = 4.0;
double _value = 20;
void _updateFromDx(double dx, double width) {
final usable = width - _thumbDiameter;
if (usable <= 0) return;
var fraction = ((dx - _thumbDiameter / 2) / usable).clamp(0.0, 1.0);
if (Directionality.of(context) == TextDirection.rtl) {
fraction = 1 - fraction;
}
setState(() {
_value = (_min + fraction * (_max - _min)).roundToDouble();
});
}
Widget _buildNativeTrack(double width, bool isRtl) {
final usable = width - _thumbDiameter;
final valueFraction = ((_value - _min) / (_max - _min)).clamp(0.0, 1.0);
final fraction = isRtl ? 1 - valueFraction : valueFraction;
final thumbCenter = _thumbDiameter / 2 + fraction * usable;
final fillLeft = isRtl ? thumbCenter : 0.0;
final fillWidth = isRtl ? width - thumbCenter : thumbCenter;
return SizedBox(
height: _thumbDiameter + 12,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: 0,
right: 0,
top: (_thumbDiameter - _trackHeight) / 2 + _trackTopInset,
child: Container(
height: _trackHeight,
decoration: BoxDecoration(
color: _trackColorUnfilled,
borderRadius: BorderRadius.circular(_trackHeight / 2),
),
),
),
Positioned(
left: fillLeft,
top: (_thumbDiameter - _trackHeight) / 2 + _trackTopInset,
child: Container(
width: fillWidth,
height: _trackHeight,
decoration: BoxDecoration(
color: _trackColorFilled,
borderRadius: BorderRadius.circular(_trackHeight / 2),
),
),
),
for (final tick in _ticks)
Positioned(
left: (_thumbDiameter / 2 + (tick / 100) * usable - 0.5)
.roundToDouble(),
top: _thumbDiameter + _trackTopInset,
child: Container(width: 1, height: 5, color: _tickColor),
),
Positioned(
left: thumbCenter - _thumbDiameter / 2,
top: _trackTopInset,
child: Container(
width: _thumbDiameter,
height: _thumbDiameter,
decoration: const BoxDecoration(
color: _trackColorFilled,
shape: BoxShape.circle,
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Style(
'mx-auto max-w-sm p-6',
children: [
const Style(
'block text-sm font-medium text-gray-900',
child: Text('Max Volume'),
),
Style('mt-1 w-full', value: _value, min: _min, max: _max),
LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
onHorizontalDragUpdate: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
child: _buildNativeTrack(constraints.maxWidth, isRtl),
);
},
),
],
);
}
}
class RangeInputs5Example extends StatefulWidget {
const RangeInputs5Example({super.key});
@override
State<RangeInputs5Example> createState() => _RangeInputs5ExampleState();
}
class _RangeInputs5ExampleState extends State<RangeInputs5Example> {
static const _min = 0.0;
static const _max = 100.0;
static const _thumbDiameter = 14.0;
static const _trackHeight = 8.0;
static const _trackColorUnfilled = Color(0xFFEFEFEF);
static const _trackColorFilled = Color(0xFF0075FF);
static const _tickColor = Color(0xFF000000);
static const _ticks = [0, 25, 50, 75, 100];
static const _trackTopInset = 4.0;
static const _labelTopInset = 16.0;
double _value = 20;
void _updateFromDx(double dx, double width) {
final usable = width - _thumbDiameter;
if (usable <= 0) return;
var fraction = ((dx - _thumbDiameter / 2) / usable).clamp(0.0, 1.0);
if (Directionality.of(context) == TextDirection.rtl) {
fraction = 1 - fraction;
}
setState(() {
_value = (_min + fraction * (_max - _min)).roundToDouble();
});
}
Widget _buildNativeTrack(double width, bool isRtl) {
final usable = width - _thumbDiameter;
final valueFraction = ((_value - _min) / (_max - _min)).clamp(0.0, 1.0);
final fraction = isRtl ? 1 - valueFraction : valueFraction;
final thumbCenter = _thumbDiameter / 2 + fraction * usable;
final fillLeft = isRtl ? thumbCenter : 0.0;
final fillWidth = isRtl ? width - thumbCenter : thumbCenter;
return SizedBox(
height: _thumbDiameter + 28,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: 0,
right: 0,
top: (_thumbDiameter - _trackHeight) / 2 + _trackTopInset,
child: Container(
height: _trackHeight,
decoration: BoxDecoration(
color: _trackColorUnfilled,
borderRadius: BorderRadius.circular(_trackHeight / 2),
),
),
),
Positioned(
left: fillLeft,
top: (_thumbDiameter - _trackHeight) / 2 + _trackTopInset,
child: Container(
width: fillWidth,
height: _trackHeight,
decoration: BoxDecoration(
color: _trackColorFilled,
borderRadius: BorderRadius.circular(_trackHeight / 2),
),
),
),
for (final tick in _ticks)
Positioned(
left: (_thumbDiameter / 2 + (tick / 100) * usable - 0.5)
.roundToDouble(),
top: _thumbDiameter + _trackTopInset,
child: Container(width: 1, height: 5, color: _tickColor),
),
for (final tick in _ticks)
Positioned(
left: _thumbDiameter / 2 + (tick / 100) * usable - 5,
top: _thumbDiameter + _labelTopInset,
child: RotatedBox(
quarterTurns: 3,
child: Text(
'$tick',
style: const TextStyle(fontSize: 10, color: Colors.black87),
),
),
),
Positioned(
left: thumbCenter - _thumbDiameter / 2,
top: _trackTopInset,
child: Container(
width: _thumbDiameter,
height: _thumbDiameter,
decoration: const BoxDecoration(
color: _trackColorFilled,
shape: BoxShape.circle,
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Style(
'mx-auto max-w-sm p-6',
children: [
const Style(
'block text-sm font-medium text-gray-900',
child: Text('Max Volume'),
),
Style('mt-1 w-full', value: _value, min: _min, max: _max),
const Style(
'flex w-full flex-col justify-between [writing-mode:vertical-lr]',
),
LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
onHorizontalDragUpdate: (details) =>
_updateFromDx(details.localPosition.dx, constraints.maxWidth),
child: _buildNativeTrack(constraints.maxWidth, isRtl),
);
},
),
],
);
}
}