diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index ea3f13bd00f364a555251614f18df19c2de84d87..f2bc2ec157a679014825ee98d28ab351ff6b3ec2 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -37,6 +37,7 @@ require('bootstrap/js/popover');
 require('select2/select2.js');
 window._ = require('underscore');
 window.Dropzone = require('dropzone');
+window.Sortable = require('vendor/Sortable');
 require('mousetrap');
 require('mousetrap/plugins/pause/mousetrap-pause');
 require('./shortcuts');
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index c345fb6ce14b78422fbcf6c680b8de2dc1fbb6c2..8f30900198e43e74dce4b942f03673733cb94804 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -6,7 +6,6 @@ function requireAll(context) { return context.keys().map(context); }
 
 window.Vue = require('vue');
 window.Vue.use(require('vue-resource'));
-window.Sortable = require('vendor/Sortable');
 requireAll(require.context('./models',   true, /^\.\/.*\.(js|es6)$/));
 requireAll(require.context('./stores',   true, /^\.\/.*\.(js|es6)$/));
 requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 2a50b72c8aae4df97e58957d081f4d65b18c2a8a..38b2eb9ff14f52a86ad46e5a5d42de8b2c56db66 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,5 +1,6 @@
 /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
 /* global Flash */
+/* global Sortable */
 
 ((global) => {
   class LabelManager {
@@ -9,11 +10,12 @@
       this.otherLabels = otherLabels || $('.js-other-labels');
       this.errorMessage = 'Unable to update label prioritization at this time';
       this.emptyState = document.querySelector('#js-priority-labels-empty-state');
-      this.prioritizedLabels.sortable({
-        items: 'li',
-        placeholder: 'list-placeholder',
-        axis: 'y',
-        update: this.onPrioritySortUpdate.bind(this)
+      this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
+        filter: '.empty-message',
+        forceFallback: true,
+        fallbackClass: 'is-dragging',
+        dataIdAttr: 'data-id',
+        onUpdate: this.onPrioritySortUpdate.bind(this),
       });
       this.bindEvents();
     }
@@ -51,13 +53,13 @@
         $target = this.otherLabels;
         $from = this.prioritizedLabels;
       }
-      if ($from.find('li').length === 1) {
+      $label.detach().appendTo($target);
+      if ($from.find('li').length) {
         $from.find('.empty-message').removeClass('hidden');
       }
-      if (!$target.find('li').length) {
+      if ($target.find('> li:not(.empty-message)').length) {
         $target.find('.empty-message').addClass('hidden');
       }
-      $label.detach().appendTo($target);
       // Return if we are not persisting state
       if (!persistState) {
         return;
@@ -101,8 +103,12 @@
 
     getSortedLabelsIds() {
       const sortedIds = [];
-      this.prioritizedLabels.find('li').each(function() {
-        sortedIds.push($(this).data('id'));
+      this.prioritizedLabels.find('> li').each(function() {
+        const id = $(this).data('id');
+
+        if (id) {
+          sortedIds.push(id);
+        }
       });
       return sortedIds;
     }
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 7ce1259e015a2bc3f22d0561cf2507d76e908874..051cb9fe5c5991a3b23e0b10db79edbe8d18b74f 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,5 +1,6 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
 /* global Flash */
+/* global Sortable */
 
 (function() {
   this.Milestone = (function() {
@@ -8,11 +9,9 @@
         type: "PUT",
         url: issue_url,
         data: data,
-        success: (function(_this) {
-          return function(_data) {
-            return _this.successCallback(_data, li);
-          };
-        })(this),
+        success: function(_data) {
+          return Milestone.successCallback(_data, li);
+        },
         error: function(data) {
           return new Flash("Issue update failed", 'alert');
         },
@@ -27,11 +26,9 @@
         type: "PUT",
         url: sort_issues_url,
         data: data,
-        success: (function(_this) {
-          return function(_data) {
-            return _this.successCallback(_data);
-          };
-        })(this),
+        success: function(_data) {
+          return Milestone.successCallback(_data);
+        },
         error: function() {
           return new Flash("Issues update failed", 'alert');
         },
@@ -46,11 +43,9 @@
         type: "PUT",
         url: sort_mr_url,
         data: data,
-        success: (function(_this) {
-          return function(_data) {
-            return _this.successCallback(_data);
-          };
-        })(this),
+        success: function(_data) {
+          return Milestone.successCallback(_data);
+        },
         error: function(data) {
           return new Flash("Issue update failed", 'alert');
         },
@@ -63,11 +58,9 @@
         type: "PUT",
         url: merge_request_url,
         data: data,
-        success: (function(_this) {
-          return function(_data) {
-            return _this.successCallback(_data, li);
-          };
-        })(this),
+        success: function(_data) {
+          return Milestone.successCallback(_data, li);
+        },
         error: function(data) {
           return new Flash("Issue update failed", 'alert');
         },
@@ -81,65 +74,30 @@
         img_tag = $('<img/>');
         img_tag.attr('src', data.assignee.avatar_url);
         img_tag.addClass('avatar s16');
-        $(element).find('.assignee-icon').html(img_tag);
+        $(element).find('.assignee-icon img').replaceWith(img_tag);
       } else {
-        $(element).find('.assignee-icon').html('');
+        $(element).find('.assignee-icon').empty();
       }
       return $(element).effect('highlight');
     };
 
     function Milestone() {
       var oldMouseStart;
-      oldMouseStart = $.ui.sortable.prototype._mouseStart;
-      $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) {
-        this._trigger("beforeStart", event, this._uiHash());
-        return oldMouseStart.apply(this, [event, overrideHandle, noActivation]);
-      };
       this.bindIssuesSorting();
       this.bindMergeRequestSorting();
       this.bindTabsSwitching();
     }
 
     Milestone.prototype.bindIssuesSorting = function() {
-      return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({
-        connectWith: ".issues-sortable-list",
-        dropOnEmpty: true,
-        items: "li:not(.ui-sort-disabled)",
-        beforeStart: function(event, ui) {
-          return $(".issues-sortable-list").css("min-height", ui.item.outerHeight());
-        },
-        stop: function(event, ui) {
-          return $(".issues-sortable-list").css("min-height", "0px");
-        },
-        update: function(event, ui) {
-          var data;
-          // Prevents sorting from container which element has been removed.
-          if ($(this).find(ui.item).length > 0) {
-            data = $(this).sortable("serialize");
-            return Milestone.sortIssues(data);
-          }
-        },
-        receive: function(event, ui) {
-          var data, issue_id, issue_url, new_state;
-          new_state = $(this).data('state');
-          issue_id = ui.item.data('iid');
-          issue_url = ui.item.data('url');
-          data = (function() {
-            switch (new_state) {
-              case 'ongoing':
-                return "issue[assignee_id]=" + gon.current_user_id;
-              case 'unassigned':
-                return "issue[assignee_id]=";
-              case 'closed':
-                return "issue[state_event]=close";
-            }
-          })();
-          if ($(ui.sender).data('state') === "closed") {
-            data += "&issue[state_event]=reopen";
-          }
-          return Milestone.updateIssue(ui.item, issue_url, data);
-        }
-      }).disableSelection();
+      $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
+        this.createSortable(el, {
+          group: 'issue-list',
+          listEls: $('.issues-sortable-list'),
+          fieldName: 'issue',
+          sortCallback: Milestone.sortIssues,
+          updateCallback: Milestone.updateIssue,
+        });
+      }.bind(this));
     };
 
     Milestone.prototype.bindTabsSwitching = function() {
@@ -154,42 +112,62 @@
     };
 
     Milestone.prototype.bindMergeRequestSorting = function() {
-      return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({
-        connectWith: ".merge_requests-sortable-list",
-        dropOnEmpty: true,
-        items: "li:not(.ui-sort-disabled)",
-        beforeStart: function(event, ui) {
-          return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight());
+      $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
+        this.createSortable(el, {
+          group: 'merge-request-list',
+          listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
+          fieldName: 'merge_request',
+          sortCallback: Milestone.sortMergeRequests,
+          updateCallback: Milestone.updateMergeRequest,
+        });
+      }.bind(this));
+    };
+
+    Milestone.prototype.createSortable = function(el, opts) {
+      return Sortable.create(el, {
+        group: opts.group,
+        filter: '.is-disabled',
+        forceFallback: true,
+        onStart: function(e) {
+          opts.listEls.css('min-height', e.item.offsetHeight);
         },
-        stop: function(event, ui) {
-          return $(".merge_requests-sortable-list").css("min-height", "0px");
+        onEnd: function () {
+          opts.listEls.css("min-height", "0px");
         },
-        update: function(event, ui) {
-          var data;
-          data = $(this).sortable("serialize");
-          return Milestone.sortMergeRequests(data);
+        onUpdate: function(e) {
+          var ids = this.toArray(),
+            data;
+
+          if (ids.length) {
+            data = ids.map(function(id) {
+              return 'sortable_' + opts.fieldName + '[]=' + id;
+            }).join('&');
+
+            opts.sortCallback(data);
+          }
         },
-        receive: function(event, ui) {
-          var data, merge_request_id, merge_request_url, new_state;
-          new_state = $(this).data('state');
-          merge_request_id = ui.item.data('iid');
-          merge_request_url = ui.item.data('url');
+        onAdd: function (e) {
+          var data, issuableId, issuableUrl, newState;
+          newState = e.to.dataset.state;
+          issuableUrl = e.item.dataset.url;
           data = (function() {
-            switch (new_state) {
+            switch (newState) {
               case 'ongoing':
-                return "merge_request[assignee_id]=" + gon.current_user_id;
+                return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
               case 'unassigned':
-                return "merge_request[assignee_id]=";
+                return opts.fieldName + '[assignee_id]=';
               case 'closed':
-                return "merge_request[state_event]=close";
+                return opts.fieldName + '[state_event]=close';
             }
           })();
-          if ($(ui.sender).data('state') === "closed") {
-            data += "&merge_request[state_event]=reopen";
+          if (e.from.dataset.state === 'closed') {
+            data += '&' + opts.fieldName + '[state_event]=reopen';
           }
-          return Milestone.updateMergeRequest(ui.item, merge_request_url, data);
+
+          opts.updateCallback(e.item, issuableUrl, data);
+          this.options.onUpdate.call(this, e);
         }
-      }).disableSelection();
+      });
     };
 
     return Milestone;
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
similarity index 98%
rename from app/assets/javascripts/boards/test_utils/simulate_drag.js
rename to app/assets/javascripts/test_utils/simulate_drag.js
index f05780167bf8636cb9e1d3969e44ee5d6853c832..7dba5840c8a8df52ae63f75e424661c1baf19ead 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -50,14 +50,15 @@
     return (
       children[target.index] ||
       children[target.index === 'first' ? 0 : -1] ||
-      children[target.index === 'last' ? children.length - 1 : -1]
+      children[target.index === 'last' ? children.length - 1 : -1] ||
+      el
     );
   }
 
   function getRect(el) {
     var rect = el.getBoundingClientRect();
     var width = rect.right - rect.left;
-    var height = rect.bottom - rect.top;
+    var height = rect.bottom - rect.top + 10;
 
     return {
       x: rect.left,
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 18f2f316f02124b3eb2a95466cf6316943151605..8487384aed6a33845be06d5ea69a5d365a6c3086 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -70,14 +70,3 @@
     }
   }
 }
-
-.ui-sortable-handle {
-  cursor: move;
-  cursor: -webkit-grab;
-  cursor: -moz-grab;
-
-  &:active {
-    cursor: -webkit-grabbing;
-    cursor: -moz-grabbing;
-  }
-}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 762b95a657c921429dbe8b5e04700526e23f9694..e1ef0b029a59a76a3c60cb3b5c5d7aa4b292ec9b 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -116,6 +116,22 @@
 }
 
 .manage-labels-list {
+  > li:not(.empty-message) {
+    background-color: $white-light;
+    cursor: move;
+    cursor: -webkit-grab;
+    cursor: -moz-grab;
+
+    &:active {
+      cursor: -webkit-grabbing;
+      cursor: -moz-grabbing;
+    }
+
+    &.sortable-ghost {
+      opacity: 0.3;
+    }
+  }
+
   .btn-action {
     color: $gl-text-color;
 
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 686b64cdd24acfe8b49fe753171bd05728a105d5..3da1150f89b7c482addbe305919f2ca3c03197ad 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -178,3 +178,9 @@
     }
   }
 }
+
+.issuable-row {
+  background-color: $white-light;
+  cursor: -webkit-grab;
+  cursor: grab;
+}
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fb6f0da28f82b2829bb75f33a5539b6e59448322..e66a8e0a3b3b4480d259821964901ea36157678c 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,8 @@
 = render "header_title"
+
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
 = render 'shared/milestones/top', milestone: @milestone, group: @group
 = render 'shared/milestones/summary', milestone: @milestone
 = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 05fe504d1c9f7da53b41484522d4f424ce86d481..f5ca9607823c1c5462c827f99c6be36db0fa9cdc 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -4,7 +4,7 @@
 
 - content_for :page_specific_javascripts do
   = page_specific_javascript_bundle_tag('boards')
-  = page_specific_javascript_bundle_tag('boards_test') if Rails.env.test?
+  = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
 
   %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
   %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 29f861c09c614955e4060c9335fc8a026c4ffcef..8d4a91cb64c6418d1864f679859113d113f7120f 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,6 +3,9 @@
 - hide_class = ''
 = render "projects/issues/head"
 
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
 - if @labels.exists? || @prioritized_labels.exists?
   %div{ class: container_class }
     .top-area.adjust
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index c3a6096aa548c5c1fb4de5b768dc4c7cc9c7ed4b..06a31698ee6d7e68ff51336ae0b7338b7d36f1df 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,6 +3,9 @@
 - page_description @milestone.description
 = render "projects/issues/head"
 
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
 %div{ class: container_class }
   .detail-page-header.milestone-page-header
     .status-box{ class: status_box_class(@milestone) }
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 28935c8b59869e8492c580776c3c4bfee9963309..4c7d69d40d5cde03edc0531e12429582a911f56e 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -5,7 +5,7 @@
 - base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
 - can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
 
-%li{ id: dom_id(issuable, 'sortable'),  class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'),  class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) }
   %span
     - if show_project_name
       %strong #{project.name} &middot;
diff --git a/changelogs/unreleased/remove-jquery-ui-sortable.yml b/changelogs/unreleased/remove-jquery-ui-sortable.yml
new file mode 100644
index 0000000000000000000000000000000000000000..35f4782273866335beba817e77e72e23cc3bcaad
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-sortable.yml
@@ -0,0 +1,4 @@
+---
+title: Replaced jQuery UI sortable
+merge_request:
+author:
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 953ae463c86ce13ae8bf50acc214edeff1f7229f..968c0076eafb663fda0037518bd48ebb686e5688 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -17,7 +17,7 @@ var config = {
     application:          './application.js',
     blob_edit:            './blob_edit/blob_edit_bundle.js',
     boards:               './boards/boards_bundle.js',
-    boards_test:          './boards/test_utils/simulate_drag.js',
+    simulate_drag:        './test_utils/simulate_drag.js',
     cycle_analytics:      './cycle_analytics/cycle_analytics_bundle.js',
     commit_pipelines:     './commit/pipelines/pipelines_bundle.js',
     diff_notes:           './diff_notes/diff_notes_bundle.js',
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 34f47daf0e5e7f2c2a96777b5cc622fbc33c2af0..7225f38b7e5525d2f0cbd4c98f070f42bf4af49a 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
 describe 'Issue Boards', feature: true, js: true do
   include WaitForAjax
   include WaitForVueResource
+  include DragTo
 
   let(:project) { create(:empty_project, :public) }
   let(:board)   { create(:board, project: project) }
@@ -188,7 +189,7 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'moves issue to done' do
-        drag_to(list_from_index: 0, list_to_index: 2)
+        drag(list_from_index: 0, list_to_index: 2)
 
         wait_for_board_cards(1, 7)
         wait_for_board_cards(2, 2)
@@ -201,7 +202,7 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'removes all of the same issue to done' do
-        drag_to(list_from_index: 0, list_to_index: 2)
+        drag(list_from_index: 0, list_to_index: 2)
 
         wait_for_board_cards(1, 7)
         wait_for_board_cards(2, 2)
@@ -215,7 +216,7 @@ describe 'Issue Boards', feature: true, js: true do
 
     context 'lists' do
       it 'changes position of list' do
-        drag_to(list_from_index: 1, list_to_index: 0, selector: '.board-header')
+        drag(list_from_index: 1, list_to_index: 0, selector: '.board-header')
 
         wait_for_board_cards(1, 2)
         wait_for_board_cards(2, 8)
@@ -226,7 +227,7 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'issue moves between lists' do
-        drag_to(list_from_index: 0, card_index: 1, list_to_index: 1)
+        drag(list_from_index: 0, from_index: 1, list_to_index: 1)
 
         wait_for_board_cards(1, 7)
         wait_for_board_cards(2, 2)
@@ -237,7 +238,7 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'issue moves between lists' do
-        drag_to(list_from_index: 1, list_to_index: 0)
+        drag(list_from_index: 1, list_to_index: 0)
 
         wait_for_board_cards(1, 9)
         wait_for_board_cards(2, 1)
@@ -248,7 +249,7 @@ describe 'Issue Boards', feature: true, js: true do
       end
 
       it 'issue moves from done' do
-        drag_to(list_from_index: 2, list_to_index: 1)
+        drag(list_from_index: 2, list_to_index: 1)
 
         expect(find('.board:nth-child(2)')).to have_content(issue8.title)
 
@@ -615,14 +616,13 @@ describe 'Issue Boards', feature: true, js: true do
     end
   end
 
-  def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
-    evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
-
-    Timeout.timeout(Capybara.default_max_wait_time) do
-      loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
-    end
-
-    wait_for_vue_resource
+  def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+    drag_to(selector: selector,
+            scrollable: '#board-app',
+            list_from_index: list_from_index,
+            from_index: from_index,
+            to_index: to_index,
+            list_to_index: list_to_index)
   end
 
   def wait_for_board_cards(board_number, expected_cards)
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index aadd72a9f8e1ace7b303a70b3ccbd62708035831..8de9942c54ecca850785a61eac4c151952856391 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
 
 describe 'Milestone draggable', feature: true, js: true do
   include WaitForAjax
+  include DragTo
 
   let(:milestone) { create(:milestone, project: project, title: 8.14) }
   let(:project)   { create(:empty_project, :public) }
@@ -75,7 +76,7 @@ describe 'Milestone draggable', feature: true, js: true do
     create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone))
 
     visit namespace_project_milestone_path(project.namespace, project, milestone)
-    issue.drag_to(issue_target)
+    drag_to(selector: '.issues-sortable-list', list_to_index: 1)
 
     wait_for_ajax
   end
@@ -85,7 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do
 
     visit namespace_project_milestone_path(project.namespace, project, milestone)
     page.find("a[href='#tab-merge-requests']").click
-    merge_request.drag_to(merge_request_target)
+    drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
 
     wait_for_ajax
   end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 97ce9cdfd87b2838cdb128d3fc824a046ba4e94c..1e900d7e660d799bc14a44f0c183260fb185922e 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
 
 feature 'Prioritize labels', feature: true do
   include WaitForAjax
+  include DragTo
 
   let(:user)     { create(:user) }
   let(:group)    { create(:group) }
@@ -99,7 +100,7 @@ feature 'Prioritize labels', feature: true do
       expect(page).to have_content 'wontfix'
 
       # Sort labels
-      find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
+      drag_to(selector: '.js-prioritized-labels', from_index: 1, to_index: 2)
 
       page.within('.prioritized-labels') do
         expect(first('li')).to have_content('feature')
diff --git a/spec/support/drag_to_helper.rb b/spec/support/drag_to_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c0659d3ecdff8430831dfe553d6e537e4c0e67f
--- /dev/null
+++ b/spec/support/drag_to_helper.rb
@@ -0,0 +1,13 @@
+module DragTo
+  def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body')
+    evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+    Timeout.timeout(Capybara.default_max_wait_time) do
+      loop until drag_active?
+    end
+  end
+
+  def drag_active?
+    page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+  end
+end