<body ng-app="app" ng-controller="controller">
   
   <range-slider config="config" model="model"></range-slider>
   <hr />
   <div class="row">
      <div>
         <pre>model = {{model | json}}</pre>
      </div>
      <div>
         <pre>config = {{config | json}}</pre>
      </div>
      <div>
         <p><input type="number" ng-model="config.min" /> <span>Min</span></p>
         <p><input type="number" ng-model="config.max" /> <span>Max</span></p>
         <p><input type="number" ng-model="config.gap" step="0.1" /> <span>Gap</span></p>
         <p><input type="number" ng-model="config.step" step="0.01" /> <span>Step</span></p>
         <p><input type="number" ng-model="config.precision" /> <span>Precision</span></p>
      </div>
   </div>
</body>
$dark: #212429;
$light: #fff;
$min: #17b5fa;
$gap: #9ccf5e;
$max: #f45035;

@mixin slider-thumbs() {
   pointer-events: all;
   position: relative;
   z-index: 9;
   outline: 0;
   width: 1em;
   height: 1em;
   z-index: 10;
   background: $light;
   box-shadow: 0 .15em .5em 0 rgba($dark, .35);
   border-radius: 2em;
   cursor: col-resize;
   border: 0;
}
@mixin slider-tracks() {
   pointer-events: none;
   position: absolute;
   padding: 0;
   margin: 0;
   top: 0;
   right: 0;
   bottom: 0;
   left: 0;
   height: 100%;
   width: 100%;
   outline: none;
   border-radius: 1em;
   background: none;
   border: 0;
}

.range-slider {
   position: relative;
   width: 100%;
   display: flex;
   flex-direction: row;
   align-items: center;
   padding: 0 0 1.5em 0;
   font-size: 4.5vmin;
   input {
      -webkit-appearance: none;
      -moz-appearance: none;
      font-size: 1em;
      z-index: 1;
      @include slider-tracks();

      &:active,
      &:focus {
         outline: 0;
         z-index: 2;
      }
      &::-moz-focus-outer {
         border: 0;
      }

      // slider track
      &::-webkit-slider-runnable-track {
         margin: 0 -.5em;
      }
      &::-moz-range-track {
         @include slider-tracks();
      }

      // slider thumbs
      &::-webkit-slider-thumb {
         -webkit-appearance: none;
         @include slider-thumbs();
      }
      &::-moz-range-thumb {
         -moz-appearance: none;
         @include slider-thumbs();
      }
   }
   .label {
      padding: 1em;
      user-select: none;
      pointer-events: none;
   }
}

.range-slider-set {
   position: relative;
   flex: 1;
   height: .5em;
   border-radius: 1em;
   background: rgba(mix($dark, $light), .5);
   pointer-events: none;
   [class*='span-'] {
      pointer-events: none;
      position: absolute;
      top:0;
      bottom: 0;
      &.span-min {
         border-radius: 1em 0 0 1em;
      }
      &.span-gap[style*='left:0%'] {
         border-top-left-radius: 1em;
         border-bottom-left-radius: 1em;
      }
      &.span-gap[style*='right:0%'] {
         border-top-right-radius: 1em;
         border-bottom-right-radius: 1em;
      }
      &.span-max {
         border-radius: 0 1em 1em 0;
      }
   }
   .span-min {background: $min linear-gradient(90deg, $min 50%, $gap 100%);}
   .span-gap {background: $gap;}
   .span-max {background: $max linear-gradient(90deg, $gap 0%, $max 50%);}
   .handle {
      position: absolute;
      top: 100%;
      left: 50%;
      line-height: 1;
      white-space: nowrap;
      text-align: center;
      width: auto;
      cursor: pointer;
      z-index: 3;
      &.from-value {
         transform: translate(-100%, 50%);
      }
      &.to-value {
         transform: translate(0%, 50%);
      }
   }
}

// boring
body {
   background: $dark;
   color: $light;
   font-family: sans-serif;
}
.row {
   display: flex;
   justify-content: space-between;
   font-size: 2.5vmin;
   > div {
      flex: 1;
      padding: 1.5em;
   }
}
input {
   border: 2px solid;
   padding: .5em;
   border-radius: .4em;
   font-size: inherit;
   font-family: inherit;
   width: 3em;
   text-align: right;
}
View Compiled
(function(angular, _) {
   'use strict';

   angular
      .module('app', [])
      .controller('controller', ['$scope', function($scope) {
         $scope.config = {};
         $scope.model = {from:65, to:80};
      }])

      .directive('rangeSlider', [function() {

         return {
            restrict: 'E',
            replace: true,
            scope: {
               config: '=?',
               model: '=',
            },

            template: '' +
               '<div class="range-slider">' +
                  '<div class="label min-value"><span>{{config.min}}</span></div>' +

                  '<div class="range-slider-set">' +
                     '<div class="span-min" style="left:0;right:{{(100 - _handleMinX)}}%"></div>' +
                     '<div class="span-gap" style="left:{{_handleMinX}}%;right:{{(100 - _handleMaxX)}}%"></div>' +
                     '<div class="span-max" style="left:{{_handleMaxX}}%;right:0"></div>' +

                     '<input type="range" class="slider-min" ng-change="_which=0" ng-model="_model[0]" min="{{_values.min}}" max="{{_values.max}}" step="{{_step}}" />' +
                     '<input type="range" class="slider-max" ng-change="_which=1" ng-model="_model[1]" min="{{_values.min}}" max="{{_values.max}}" step="{{_step}}" />' +

                     '<div class="handle from-value" style="left:{{_handleMinX}}%"><span>{{_model[0]|number:config.precision}}</span></div>' +
                     '<div class="handle to-value" style="left:{{_handleMaxX}}%"><span>{{_model[1]|number:config.precision}}</span></div>' +
                  '</div>' +

                  '<div class="label max-value"><span>{{config.max}}</span></div>' +
               '</div>',

            link: function link(scope, element) {
               var defaultConfig = {
                  min: 0,
                  max: 100,
                  gap: 10,
                  step: 0.1,
                  precision: 1,
               };

               scope.config = _.extend({}, angular.copy(defaultConfig), scope.config);

               scope._model = [
                  scope.model.from,
                  scope.model.to
               ];

               scope._values = {
                  min: scope.config.min || 0,
                  max: scope.config.max || 100
               };

               scope._labels = {
                  min: scope._values.min,
                  max: scope._values.max,
               };

               scope._step = scope.config.step || 1;

               scope._gap = parseFloat(scope.config.gap) || 0;

               // Responsible for determining which slider the user was moving,
               // which help us resolve occurrences of sliders overlapping.
               scope._which = 0;

               var _notInRunLoop = function _notInRunLoop() {
                  return !scope.$root.$$phase;
               };

               var _reevaluateInputs = function() {
                  var inputElements = element.find('input');

                  _.each(inputElements, function(inputElement, index) {
                     inputElement = angular.element(inputElement);

                     inputElement.val(scope._model[index]);
                  });
               };

               var _updateModel = function _updateModel(model) {
                  scope.model = {
                     from: parseFloat((model[0]).toFixed(scope.config.precision)),
                     to: parseFloat((model[1]).toFixed(scope.config.precision)),
                  };
                  
                  scope._handleMinX = ((scope._model[0] - scope._values.min) / (scope._values.max - scope._values.min)) * 100;
                  scope._handleMaxX = ((scope._model[1] - scope._values.min) / (scope._values.max - scope._values.min)) * 100;

                  scope._handleMinX = scope._handleMinX < 0 ? 0 : scope._handleMinX;
                  scope._handleMaxX = scope._handleMaxX > 100 ? 100 : scope._handleMaxX;

                  if (_notInRunLoop()) {
                     try {
                        // Sometimes we're outside of the Angular run-loop,
                        // and therefore need to manually invoke the `apply` method!
                        scope.$apply();
                     } catch (e) {}
                  }
               };

               scope.$watch('config', function(n, o) {
                  scope.config = _.extend({}, angular.copy(defaultConfig), scope.config);
                  if (n && o && n !== o) {
                     scope.config.min = parseFloat(scope.config.min);
                     scope.config.max = parseFloat(scope.config.max);
                     scope.config.gap = parseFloat(scope.config.gap);

                     // fix the gap
                     if (scope.config.gap > scope.config.max - scope.config.min) {
                        scope.config.gap = scope.config.max - scope.config.min;
                     }

                     scope.config.step = parseFloat(scope.config.step);
                     scope._gap = scope.config.gap;
                     scope._step = scope.config.step;

                     scope._values = {
                        min: scope.config.min || 0,
                        max: scope.config.max || 100
                     };

                     scope._labels = {
                        min: scope.config.labels.min || scope._values.min,
                        max: scope.config.labels.max || scope._values.max
                     };

                     _reevaluateInputs();
                  }
               }, true);

               // Listen for any changes to the original model.
               scope.$watch('model', function alteredValues() {
                  scope._model = [scope.model.from, scope.model.to];
                  _reevaluateInputs();
               }, true);

               // Observe the `_model` for any changes.
               scope.$watchCollection('_model', function modelChanged() {
                  scope._model[0] = parseFloat(scope._model[0])*1;
                  scope._model[1] = parseFloat(scope._model[1])*1;

                  // User was moving the first slider.
                  if (scope._which === 0 && scope._model[1] - scope._gap < scope._model[0]) {
                     scope._model[1] = scope._model[0] + scope._gap;
                  }

                  // Otherwise they were moving the second slider.
                  if (scope._which === 1 && scope._model[0] + scope._gap > scope._model[1]) {
                     scope._model[0] = scope._model[1] - scope._gap;
                  }

                  // Constrain to the min/max values.
                  (function constrainMinMax() {
                     if (scope._model[0] < scope._values.min) {
                        scope._model[0] = scope._values.min;
                     }

                     if (scope._model[1] < scope._values.min) {
                        scope._model[1] = scope._values.min;
                     }

                     if (scope._model[0] > scope._values.max) {
                        scope._model[0] = scope._values.max;
                     }

                     if (scope._model[1] > scope._values.max) {
                        scope._model[1] = scope._values.max;
                     }

                     // mind the gap
                     var _m0Gap = scope._model[0] + scope._gap;
                     var _m1Gap = scope._model[1] - scope._gap;

                     if (scope._model[0] > _m1Gap) {
                        scope._model[0] = _m1Gap;
                     }

                     if (scope._model[1] < _m0Gap) {
                        scope._model[1] = _m0Gap;
                     }
                  })();

                  // Update the model!
                  _updateModel(scope._model);
                  _reevaluateInputs();
               });
            }
         };
      }]);

})(window.angular, window._);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.2/angular.min.js
  2. //cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js