Multi Thumb Range Slider

A customizable Flutter widget that provides a multi-thumb slider with draggable thumbs for defining ranges, breakpoints, or multiple selection points.

🚀 Live Example

Try the interactive example app: Live Demo

The live demo showcases all the features of the multi-thumb slider with interactive examples you can test in your browser.

Features

  • Multiple Thumbs: Set multiple values on a single slider track with intuitive drag-and-drop interaction
  • Generic Type Support: Full support for int, double, enum, and other comparable types
  • Open Segments: Support for open-ended ("80kg+") and open-started ("55kg or less") segments with visual arrows
  • Customizable Appearance: Extensive styling options for colors, sizes, and visual elements
  • Responsive Design: Adapts to different screen sizes and orientations with smooth animations
  • Tickmarks & Labels: Configurable tickmarks with positioning options (above, below, on-track)
  • Segment Display: Built-in segment visualization with multiple content types
  • Custom Formatting: Flexible value formatting for currency, percentages, weights, and more
  • Segment Editing: Add and remove segments dynamically with visual buttons

Getting Started

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  multi_thumb_range_slider: ^1.3.0

Quick Start Examples

Minimal Setup

🎬 Animated example placeholder - GIF will be added here

CustomMultiThumbSlider.withInt(
  values: [20, 80],
  onChanged: (values) => print('Values: $values'),
)

With Visual Enhancements

CustomMultiThumbSlider.withInt(
  values: [25, 75],
  showTickmarks: true,
  showTooltip: true,
  showSegments: true,
  onChanged: (values) => print('Range: ${values[0]} - ${values[1]}'),
)

Interactive Editing Mode

CustomMultiThumbSlider.withInt(
  values: [30, 70],
  showSegments: true,
  enableSegmentEdit: true,
  enableDescriptionEdit: true,
  onSegmentAdd: (index) => handleSegmentAdd(index),
  onSegmentRemove: (index) => handleSegmentRemove(index),
  onChanged: (values) => setState(() => _values = values),
)

Basic Usage

Basic Integer Slider with Built-in Features

🎬 Animated example placeholder - GIF will be added here

import 'package:flutter/material.dart';
import 'package:multi_thumb_range_slider/multi_thumb_range_slider.dart';

class BasicExampleWidget extends StatefulWidget {
  @override
  State<BasicExampleWidget> createState() => _BasicExampleWidgetState();
}

class _BasicExampleWidgetState extends State<BasicExampleWidget> {
  List<int> _values = [20, 50, 80];

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider.withInt(
      values: _values,
      min: 0,
      max: 100,
      // Visual enhancements
      showTickmarks: true,
      tickmarkInterval: 5,
      showTickmarkLabels: true,
      tickmarkLabelInterval: 10,
      showTooltip: true,
      valueFormatter: (value) => '$value%',
      // Built-in segment display
      showSegments: true,
      segmentContentType: SegmentContentType.fromToRange,
      onChanged: (newValues) {
        setState(() {
          _values = newValues;
        });
      },
    );
  }
}

Double Precision Slider with Decimal Formatting

🎬 Animated example placeholder - GIF will be added here

class DoubleExampleWidget extends StatefulWidget {
  @override
  State<DoubleExampleWidget> createState() => _DoubleExampleWidgetState();
}

class _DoubleExampleWidgetState extends State<DoubleExampleWidget> {
  List<double> _values = [15.5, 42.3, 78.9];

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider<double>(
      values: _values,
      min: 0.0,
      max: 100.0,
      showTooltip: true,
      valueFormatter: (value) => value.toStringAsFixed(1),
      // Show segment widths with decimal precision
      showSegments: true,
      segmentContentType: SegmentContentType.width,
      onChanged: (newValues) {
        setState(() {
          _values = newValues;
        });
      },
    );
  }
}

Enum Slider with Educational Dan Rank System

🎬 Animated example placeholder - GIF will be added here

enum DanRank { 
  firstDan, secondDan, thirdDan, fourthDan, fifthDan,
  sixthDan, seventhDan, eighthDan, ninthDan, tenthDan 
}

extension DanRankExtension on DanRank {
  String get displayName {
    switch (this) {
      case DanRank.firstDan: return '1st Dan';
      case DanRank.secondDan: return '2nd Dan';
      // ... more cases
      default: return '${index + 1}th Dan';
    }
  }
}

class DanRankExampleWidget extends StatefulWidget {
  @override
  State<DanRankExampleWidget> createState() => _DanRankExampleWidgetState();
}

class _DanRankExampleWidgetState extends State<DanRankExampleWidget> {
  List<DanRank> _values = [DanRank.fifthDan, DanRank.eighthDan];

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider.withEnum<DanRank>(
      values: _values,
      min: DanRank.firstDan,
      max: DanRank.tenthDan,
      allPossibleValues: DanRank.values,
      showTickmarks: true,
      showTickmarkLabels: true,
      tickmarkLabelInterval: 1,
      showTooltip: true,
      valueFormatter: (value) => value.displayName,
      onChanged: (newValues) {
        setState(() {
          _values = newValues;
        });
      },
    );
  }
}

Advanced Examples

Price Range Selector

🎬 Animated example placeholder - GIF will be added here

Applications with currency formatting and range categorization:

class PriceRangeExampleWidget extends StatefulWidget {
  @override
  State<PriceRangeExampleWidget> createState() => _PriceRangeExampleWidgetState();
}

class _PriceRangeExampleWidgetState extends State<PriceRangeExampleWidget> {
  List<int> _values = [50, 150, 300];

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider.withInt(
      values: _values,
      min: 0,
      max: 500,
      showTooltip: true,
      valueFormatter: (value) => '\$$value',
      // E-commerce styled segments
      showSegments: true,
      segmentContentType: SegmentContentType.toRange,
      segmentCardBackgroundColor: Colors.green.shade50,
      segmentCardBorderColor: Colors.green.shade200,
      segmentTextColor: Colors.green.shade800,
      onChanged: (newValues) {
        setState(() {
          _values = newValues;
        });
      },
    );
  }
}

Weight Class Selector for Sports Applications

🎬 Animated example placeholder - GIF will be added here

Weight class ranges:

class WeightClassExampleWidget extends StatefulWidget {
  @override
  State<WeightClassExampleWidget> createState() => _WeightClassExampleWidgetState();
}

class _WeightClassExampleWidgetState extends State<WeightClassExampleWidget> {
  List<int> _values = [60, 70, 80, 90];

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider.withInt(
      values: _values,
      min: 50,
      max: 100,
      height: 60,
      thumbRadius: 18,
      showTooltip: true,
      valueFormatter: (value) => '${value}kg',
      onChanged: (newValues) {
        setState(() {
          _values = newValues;
        });
      },
    );
  }
}

Interactive Segment Editing

🎬 Animated example placeholder - GIF will be added here

Advanced segment editing capabilities with add/remove functionality:

class SegmentEditExampleWidget extends StatefulWidget {
  @override
  State<SegmentEditExampleWidget> createState() => _SegmentEditExampleWidgetState();
}

class _SegmentEditExampleWidgetState extends State<SegmentEditExampleWidget> {
  List<int> _values = [25, 50, 75];
  bool _editModeEnabled = true;
  bool _descriptionEditEnabled = true;

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider.withInt(
      values: _values,
      min: 0,
      max: 100,
      // Enable segment display and editing
      showSegments: true,
      segmentContentType: SegmentContentType.fromToRange,
      enableSegmentEdit: _editModeEnabled,
      enableDescriptionEdit: _descriptionEditEnabled,
      // Optional editing callbacks
      onSegmentAdd: (segmentIndex) {
        // Handle adding new segment
        final newValues = calculateNewValues(segmentIndex);
        setState(() => _values = newValues);
      },
      onSegmentRemove: (segmentIndex) {
        // Handle removing segment
        final newValues = calculateRemovedValues(segmentIndex);
        setState(() => _values = newValues);
      },
      onDescriptionChanged: (segmentIndex, description) {
        // Handle custom description changes
        print('Segment $segmentIndex: $description');
      },
      onChanged: (newValues) {
        setState(() {
          _values = newValues;
        });
      },
    );
  }
}

Tickmark & Visual Features

Tickmark Positioning Options

🎬 Animated example placeholder - GIF will be added here

Configure tickmarks to appear above, below, or on the track itself:

class TickmarkPositioningExample extends StatefulWidget {
  @override
  State<TickmarkPositioningExample> createState() => _TickmarkPositioningExampleState();
}

class _TickmarkPositioningExampleState extends State<TickmarkPositioningExample> {
  List<int> _values = [25, 75];
  TickmarkPosition _position = TickmarkPosition.below;

  @override
  Widget build(BuildContext context) {
    return CustomMultiThumbSlider<int>(
      values: _values,
      min: 0,
      max: 100,
      // Tickmark configuration
      showTickmarks: true,
      tickmarkInterval: 10,
      tickmarkSize: 8.0,
      tickmarkPosition: _position,     // above, below, or onTrack
      tickmarkSpacing: 8.0,           // Distance from track
      // Label configuration  
      showTickmarkLabels: true,
      tickmarkLabelInterval: 20,
      labelSpacing: 4.0,              // Distance from tickmarks
      onChanged: (newValues) => setState(() => _values = newValues),
    );
  }
}

Tooltip Configuration

Interactive tooltips with custom formatting:

CustomMultiThumbSlider.withInt(
  values: [20, 50, 80],
  showTooltip: true,
  tooltipColor: Colors.blue.shade700,
  tooltipTextColor: Colors.white,
  tooltipTextSize: 14.0,
  valueFormatter: (value) => '$value%',    // Custom formatting
  onChanged: (newValues) => setState(() => _values = newValues),
)

Custom Styling and Color Schemes

Extensive styling options for different themes:

CustomMultiThumbSlider.withInt(
  values: [30, 60, 90],
  min: 0,
  max: 100,
  // Track styling
  height: 50,
  trackHeight: 8.0,
  trackColor: Colors.grey[300]!,
  // Thumb styling
  thumbColor: Colors.blue[600]!,
  thumbRadius: 16,
  // Range colors (cycles if more ranges than colors)
  rangeColors: [
    Colors.green.shade200,
    Colors.blue.shade200,
    Colors.purple.shade200,
    Colors.red.shade200,
  ],
  onChanged: (newValues) => setState(() => _values = newValues),
)

Segment Display & Editing Features

The multi-thumb range slider includes segment visualization and editing capabilities for interactive application use cases.

Segment Display Options

🎬 Animated example placeholder - GIF will be added here

The segment display supports three different content types:

// 1. From-To Range Display (shows "0-20", "20-50", "50-100")
CustomMultiThumbSlider.withInt(
  values: [20, 50, 80],
  showSegments: true,
  segmentContentType: SegmentContentType.fromToRange,
  onChanged: (newValues) => setState(() => _values = newValues),
)

// 2. To Range Display (shows "-20", "-50", "-100")  
CustomMultiThumbSlider.withInt(
  values: [20, 50, 80],
  showSegments: true,
  segmentContentType: SegmentContentType.toRange,
  onChanged: (newValues) => setState(() => _values = newValues),
)

// 3. Width Display (shows calculated segment widths "20", "30", "20")
CustomMultiThumbSlider.withInt(
  values: [20, 50, 80],
  showSegments: true,
  segmentContentType: SegmentContentType.width,
  onChanged: (newValues) => setState(() => _values = newValues),
)

Segment Editing Capabilities

Enable dynamic segment editing with visual add/remove buttons:

CustomMultiThumbSlider.withInt(
  values: [25, 50, 75],
  showSegments: true,
  enableSegmentEdit: true,              // Enable add/remove buttons
  enableDescriptionEdit: true,          // Enable description editing
  onSegmentAdd: (index) {
    // Add new segment at specified position
  },
  onSegmentRemove: (index) {
    // Remove segment at specified position  
  },
  onDescriptionChanged: (index, description) {
    // Handle custom description changes
  },
  onChanged: (newValues) => setState(() => _values = newValues),
)

Custom Segment Styling

Customize the appearance of segment cards with extensive styling options:

CustomMultiThumbSlider.withInt(
  values: [20, 50, 80],
  showSegments: true,
  segmentContentType: SegmentContentType.fromToRange,
  // Segment styling
  segmentHeight: 70,
  segmentCardPadding: 12,
  segmentCardMargin: 4,
  segmentCardBorderRadius: 12,
  segmentCardBackgroundColor: Colors.purple.shade100,
  segmentCardBorderColor: Colors.purple.shade400,
  segmentTextColor: Colors.purple.shade900,
  segmentTextSize: 14,
  segmentTextWeight: FontWeight.bold,
  showSegmentBorders: true,
  showSegmentBackgrounds: true,
  valueFormatter: (value) => '${value}%',
  onChanged: (newValues) => setState(() => _values = newValues),
)

Open Segments

The multi-thumb slider supports open segments that extend infinitely in one direction, perfect for representing ranges like "55kg or less" or "80kg and above".

Open-Ended Segments

Enable segments with no upper bound using enableOpenEndedSegment:

CustomMultiThumbSlider.withInt(
  values: [60, 80],
  min: 40,
  max: 120,
  enableOpenEndedSegment: true,
  showSegments: true,
  segmentContentType: SegmentContentType.fromToRange,
  valueFormatter: (value) => '${value}kg',
  onChanged: (values) => print(values),
)

This creates segments:

  • 40kg - 60kg (Light)
  • 60kg - 80kg (Medium)
  • 80kg+ (Heavy) ← Open-ended with right arrow

Open-Started Segments

Enable segments with no lower bound using enableOpenStartedSegment:

CustomMultiThumbSlider.withInt(
  values: [55, 75],
  min: 40,
  max: 120,
  enableOpenStartedSegment: true,
  showSegments: true,
  segmentContentType: SegmentContentType.fromToRange,
  valueFormatter: (value) => '${value}kg',
  onChanged: (values) => print(values),
)

This creates segments:

  • - 55kg (Light) ← Open-started with left arrow
  • 55kg - 75kg (Medium)
  • 75kg - 120kg (Heavy)

Dual Open Segments

Combine both for maximum flexibility:

CustomMultiThumbSlider.withInt(
  values: [55, 75],
  min: 40,
  max: 120,
  enableOpenStartedSegment: true,
  enableOpenEndedSegment: true,
  showSegments: true,
  segmentContentType: SegmentContentType.fromToRange,
  valueFormatter: (value) => '${value}kg',
  onChanged: (values) => print(values),
)

This creates segments:

  • - 55kg (Light) ← Left arrow
  • 55kg - 75kg (Medium)
  • 75kg+ (Heavy) ← Right arrow

Visual Indicators

Open segments are clearly indicated with:

  • Custom painted arrows on the track itself
  • Appropriate labels (e.g., "55kg+", "- 80kg")
  • Proportional widths for visual balance

Value Formatting & Localization

Built-in formatters for common use cases with custom formatting support:

// Percentage formatting
CustomMultiThumbSlider.withInt(
  values: [20, 50, 80],
  valueFormatter: (value) => '$value%',
  onChanged: (newValues) => setState(() => _values = newValues),
)

// Currency formatting
CustomMultiThumbSlider.withInt(
  values: [50, 150, 300],
  valueFormatter: (value) => '\$$value',
  onChanged: (newValues) => setState(() => _values = newValues),
)

// Weight formatting
CustomMultiThumbSlider.withInt(
  values: [60, 80, 100],
  valueFormatter: (value) => '${value}kg',
  onChanged: (newValues) => setState(() => _values = newValues),
)

// Decimal precision formatting
CustomMultiThumbSlider<double>(
  values: [15.5, 42.3, 78.9],
  valueFormatter: (value) => value.toStringAsFixed(1),
  onChanged: (newValues) => setState(() => _values = newValues),
)

Read-Only Mode

CustomMultiThumbSlider.withInt(
  values: [25, 50, 75],
  min: 0,
  max: 100,
  readOnly: true,           // Disables all interaction
  showSegments: true,       // Can still show segment information
  showTickmarks: true,      // Visual elements remain functional
  onChanged: (values) {},   // Callback not called in read-only mode
)

API Reference

CustomMultiThumbSlider

The main widget providing multi-thumb slider functionality with support for generic value types, extensive visual customization, and interactive features.

Constructors

Generic Constructor
const CustomMultiThumbSlider<T>({
  Key? key,
  required List<T> values,
  required ValueChanged<List<T>> onChanged,
  required T min,
  required T max,
  
  // Basic Layout
  double height = 45.0,
  double trackHeight = 8.0,
  bool readOnly = false,
  
  // Colors & Styling  
  Color trackColor = const Color(0xFFE0E0E0),
  List<Color> rangeColors = defaultRangeColors,
  Color thumbColor = Colors.white,
  double thumbRadius = 14.0,
  
  // Value Formatting
  String Function(T)? valueFormatter,
  List<T>? allPossibleValues, // Required for enum types
  
  // Tickmarks & Labels
  bool showTickmarks = false,
  int? tickmarkInterval,
  double tickmarkSize = 6.0,
  Color tickmarkColor = Colors.grey,
  TickmarkPosition tickmarkPosition = TickmarkPosition.below,
  double tickmarkSpacing = 6.0,
  bool showTickmarkLabels = false,
  int? tickmarkLabelInterval,
  double tickmarkLabelSize = 12.0,
  Color tickmarkLabelColor = Colors.black87,
  double labelSpacing = 4.0,
  
  // Tooltips
  bool showTooltip = false,
  Color tooltipColor = Colors.black87,
  Color tooltipTextColor = Colors.white,
  double tooltipTextSize = 12.0,
  
  // Segment Display
  bool showSegments = false,
  SegmentContentType segmentContentType = SegmentContentType.fromToRange,
  double segmentHeight = 60.0,
  double segmentCardPadding = 8.0,
  double segmentCardMargin = 2.0,
  double segmentCardBorderRadius = 8.0,
  Color segmentCardBackgroundColor = const Color(0xFFF5F5F5),
  Color segmentCardBorderColor = const Color(0xFFE0E0E0),
  Color segmentTextColor = const Color(0xFF424242),
  double segmentTextSize = 12.0,
  FontWeight segmentTextWeight = FontWeight.normal,
  bool showSegmentBorders = true,
  bool showSegmentBackgrounds = true,
  
  // Segment Editing
  bool enableSegmentEdit = false,
  bool enableDescriptionEdit = false,
  Color segmentAddButtonColor = Colors.green,
  Color segmentRemoveButtonColor = Colors.red,
  double segmentButtonSize = 20.0,
  Function(int)? onSegmentAdd,
  Function(int)? onSegmentRemove,
  Function(int, String?)? onDescriptionChanged,
})
Int Convenience Constructor
const CustomMultiThumbSlider.withInt({
  // Same parameters as generic constructor but with int defaults
  int min = 0,
  int max = 100,
  // ... all other parameters
})
Enum Convenience Constructor
const CustomMultiThumbSlider.withEnum<T extends Enum>({
  required List<T> allPossibleValues,
  // ... all other parameters  
})

Key Parameters

Category Parameter Description
Core values List of current thumb values
min / max Value range boundaries
onChanged Callback for value changes
readOnly Disable interaction for display-only mode
Visual showTickmarks Enable tickmark display
showTooltip Enable interactive tooltips
showSegments Enable segment visualization
valueFormatter Custom value formatting function
Interaction enableSegmentEdit Enable add/remove segment buttons
enableDescriptionEdit Enable segment description editing
onSegmentAdd / onSegmentRemove Segment editing callbacks
onDescriptionChanged Description change callback
Open Segments enableOpenEndedSegment Enable open-ended segments (no upper bound)
enableOpenStartedSegment Enable open-started segments (no lower bound)

Enums

enum SegmentContentType { fromToRange, toRange, width }
enum TickmarkPosition { above, below, onTrack }

Type Support & Behavior

Type Usage Behavior
int Most common use case Values automatically rounded to nearest integer
double Precise measurements Full decimal precision maintained
enum Categorical selection Discrete values with custom display names
Custom Any comparable type Must implement comparison operators

Type-Specific Features

  • All types: Tooltips, segments, tickmarks, formatting
  • Int/Double: Automatic interval calculation for tickmarks
  • Enums: Requires allPossibleValues parameter for proper spacing
  • Custom types: Must provide custom valueFormatter for display

Customization Options

Visual Theming

  • Colors: Track, thumb, range, and segment colors
  • Sizing: Height, radius, padding, and spacing controls
  • Typography: Font sizes, weights, and colors for labels
  • Borders: Border radius, width, and styling options

Layout Control

  • Positioning: Tickmarks above, below, or on track
  • Spacing: Configurable gaps between elements
  • Responsive: Automatic adaptation to screen sizes
  • Overflow: Smart handling of limited space

Interaction Design

  • Touch Targets: Optimized thumb sizes for mobile
  • Visual Feedback: Hover states and selection indicators
  • Accessibility: Screen reader and keyboard navigation support
  • Animation: Smooth transitions and state changes

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

If you encounter any issues or have questions, please:

  1. Check the existing issues
  2. Create a new issue with a detailed description
  3. Include your Flutter version and device information

Changelog

1.3.0

  • Custom Segment Descriptions: Interactive editing of segment descriptions
    • Tap-to-edit functionality with popup dialogs
    • Reset to default button for easy restoration
    • Visual indicators for customized segments
    • onDescriptionChanged callback for monitoring changes
  • Segment Data Access: New getSegmentsWithDescriptions() method
    • Returns complete segment information including custom descriptions
    • SliderSegment<T> and SegmentDescription data models
    • Full type safety and generic support
  • Enhanced UX: Improved segment editing experience with better visual feedback

1.2.0

  • New Segment Display Feature: Built-in segment visualization above the slider
    • Three content types: from-to range, to range, and width display
    • Extensive styling customization options
    • Integration with value formatters
    • Works with numeric types (int, double)
  • Enhanced Examples: New examples showcasing segment display features
  • Improved Documentation: Comprehensive examples and API documentation

1.1.0

  • Added generic type support for int, double, enum, and other types
  • Changed default type from double to int
  • Added convenience constructor CustomMultiThumbSlider.int() with default min/max values
  • Improved type safety and flexibility

1.0.0

  • Initial release
  • Multi-thumb slider with draggable interface
  • Customizable appearance and styling
  • Range constraint enforcement
  • Responsive design support