Grow your CSS skills. Land your dream job.

Draggable Elements That Push Others Out Of Way

Published by Chris Coyier

Aside from a few esoteric tricks involving stuff like the resize handle on textareas, draggable elements is JavaScript territory on the web. E.g. click on element, hold down mouse button, drag mouse cursor, element drags with the mouse, release mouse button to release element. Or the touch equivalents. Fortunately this is well-tread territory. Time tested tools like jQuery UI offer Draggable (and other similar methods) to make this easy.

But recently in trying to achieve a certain effect (see title of this article) I couldn't quite get jQuery UI to do it how I wanted. But I got it done and here's how.

I was trying to recreate the effect in the sidebar in Keynote where you can drag to rearrange slides. Here's the finished effect:

You can get super close with some really basic jQuery UI configuration. We're using the Sortable method here. This is specifically for sorting lists, which is what we are doing, which is a bit like using both Draggable and Droppable.

$(".all-slides").sortable({
  
  axis: "y",
  revert: true,
  scroll: false,
  placeholder: "sortable-placeholder",
  cursor: "move"

});

On some basic HTML like this:

<div class='all-slides'>

  <div class='slide'>Slide</div>
  <div class='slide'>Slide</div>
  <div class='slide'>Slide</div>

  <!-- etc -->

The "placeholder" in the configuration is an element (with the provided class name) that gets inserted amongst the slides where that slide would drop if you released the mouse button right now. You can style that however you want with CSS, so I made it look like the blue-line-on-the-left like Keynote.

The trouble here is we don't get that "push out of the way" effect we're going for. The placeholder just shows up immediately. Originally I tried to solve it by using a @keyframes animation to expand the height of the placeholder from 0 to the slide height and then stay there with fill-mode. That works for the appearance of the placeholder, but jQuery UI just rips that placeholder out of the DOM when it goes away which didn't allow for a graceful exit.

So some deeper trickery was in order. Fortunately, after bemoaning the difficulty on Twitter, AJ Kandy found a handy example doing just what I needed.

Bear with me, this gets a bit complicated:

  1. Loop through all the slides
  2. Create a duplicate of each slide
  3. Position the slide directly on top of the original
  4. Hide it
  5. Save a reference to the slide it was duplicated from

Then you initiate the Sortable method on just the originals. Then when you start dragging a slide:

  1. Hide all the original slides
  2. Reveal the clones
  3. As you drag around, the originals will be re-arranging invisibly
  4. After they do that, animate the clones to those new positions

When the dragging stops:

  1. Make sure all the clones are in the right final position
  2. Swap out the visibility again, revealing the originals

It took a good amount of time to wrap my head around and tinker with to get just right. In my example I use psuedo-elements to number the slides, so there is a bit of code for that as well. Here it is all together.

$(".slide").each(function(i) {
  var item = $(this);
  var item_clone = item.clone();
  item.data("clone", item_clone);
  var position = item.position();
  item_clone
  .css({
    left: position.left,
    top: position.top,
    visibility: "hidden"
  })
    .attr("data-pos", i+1);
  
  $("#cloned-slides").append(item_clone);
});

$(".all-slides").sortable({
  
  axis: "y",
  revert: true,
  scroll: false,
  placeholder: "sortable-placeholder",
  cursor: "move",

  start: function(e, ui) {
    ui.helper.addClass("exclude-me");
    $(".all-slides .slide:not(.exclude-me)")
      .css("visibility", "hidden");
    ui.helper.data("clone").hide();
    $(".cloned-slides .slide").css("visibility", "visible");
  },

  stop: function(e, ui) {
    $(".all-slides .slide.exclude-me").each(function() {
      var item = $(this);
      var clone = item.data("clone");
      var position = item.position();

      clone.css("left", position.left);
      clone.css("top", position.top);
      clone.show();

      item.removeClass("exclude-me");
    });
    
    $(".all-slides .slide").each(function() {
      var item = $(this);
      var clone = item.data("clone");
      
      clone.attr("data-pos", item.index());
    });

    $(".all-slides .slide").css("visibility", "visible");
    $(".cloned-slides .slide").css("visibility", "hidden");
  },

  change: function(e, ui) {
    $(".all-slides .slide:not(.exclude-me)").each(function() {
      var item = $(this);
      var clone = item.data("clone");
      clone.stop(true, false);
      var position = item.position();
      clone.animate({
        left: position.left,
        top: position.top
      }, 200);
    });
  }
  
});

And the demo:

See the Pen Slide Rearranger like Keynote by Chris Coyier (@chriscoyier) on CodePen.

Bonus feature: the slides get a class when being drug which pulsates their size, like in Keynote.

Packery & Dragabilly

David DeSandro has some projects you may have heard of all about rearranging boxes on the web, like Masonry and Isotope. He also has one called Packery which uses an algorithm to pack boxes into spaces. Along with another project of his, Dragabilly, you can create the same effect where dragging an element moves others out of the way. The single-axis nature of our demo is easy work for it.

David forked my demo and made it work with those tools, and it's a good bit less code:

See the Pen Slide Rearranger like Keynote - with Packery and Draggabilly by David DeSandro (@desandro) on CodePen.

Comments

  1. Felipe
    Permalink to comment#

    It looks very nice and polished, but I got this weird behaviour in FF 27 when dragging and scrolling in the first sample. The list would stay fixed and the element being dragged would scroll. Not sure if that’s the intended behaviour or even how to fix it, just figured I’d mention it.

  2. Permalink to comment#

    I tried this in both Chrome 32 and Firefox 23, and all other slides disappeared while I was dragging the selected slide. In the start parameter of the sortable hash, I changed the first css call to .css("visibility", "visible") and now it works (albeit without the smooth push-out-of-way animation). Changing the second css from “visible” to “hidden” doesn’t result in any additional change.

    Are both issues (hiding while dragged, and no “push” animation) a result of not using the CSS shown above? I just put some quick styles in to mimic yours.

  3. Olivier Clement
    Permalink to comment#

    Interesting approach
    Personally I’ve been using Gridster for a very similar thing, and I find their method to be quite elegant, you should take a look (it’s a bit buggy if you want to do something it’s not meant to but it works)

    In short, they calculate the absolute position of all elements and create a “faux grid” in memory, and generate a style sheet with the position of each row/column. These styles are the. applied to the elements.

    With this it is easy to use CSS animation to have the push effect you were after

  4. What’s the last (forked) one supposed to do? In both Firefox (27) and Chrome (32) as well as IE11 it seems to keep the “dragged” item in its original place.

  5. Permalink to comment#

    Here is another simpler implementation:

    $('.sortable-container').sortable({
        placeholder: "test-placeholder",
        over: function() {
           $('.test-placeholder').stop().animate({
               height: 0
           }, 400);
        },
        change: function() {
           $('.test-placeholder').stop().animate({
               height: 50
           }, 400);
        }
    });
    

    Demo: http://jsfiddle.net/tovic/85EpZ/

  6. You can achieve the same effect by using CSS3 transitions on the “top” property as well : http://codepen.io/nuo/pen/CxLek

  7. Some time ago I was frustrated that there were very little of these smooth sortable thingys anywhere. The few that I could find were all based on jquery sortable, and it just feels icky in how it tracks the mouse cursor and when it decides to rearrange items. I really wanted to get the solid kind of feeling like what you get with reordering stuff on iOS. So here’s my stab at it: http://pumpula.net/p/smooth-sortable/

    In real world use there probably isn’t much difference between this and most other impelementation, but I’m obsessive about getting the feeling just right.

    I’m not an experienced coder, so the code is some kinda spaghetti, but I think I have the “physics” of it all pretty nicely solved. The trick is tracking the actual object boundaries and not the mouse cursor.(Just like the packery example does I think) The packery one has some lag until it rearranges items. For performance maybe? I thought that measuring the list geometry to a js object on drag start, then using that to track everything and applying new position styles to the actual DOM elements and letting css transitions do the animation and it seems to work really well. So there’s like a virtual list that’s doing all the work and a DOM list that’s animated according to the virtual list.

  8. @Ville Vanninen: Wow, I am very impressed with your work. That’s very slick, smooth and responsive too. Great job. Bookmarked.

  9. Joe Hanson
    Permalink to comment#

    You are able to drag a slide on top of another slide when using the second example with a phone.

    • Joe Hanson
      Permalink to comment#

      Edit: Apparently not anymore… It wasn’t working this morning now it is!

  10. Permalink to comment#

    It would be interesting to use this with animated content to assemble a playlist.

  11. I also want to give a shoutout to Greensock Draggable, it’s pretty much like the others: Touch support etc. but with really smooth movement, and the premium version gives you throw physics as well.

    It doesn’t come with sort logic out of the box, but you can get there pretty easily with the callback system.

  12. Héctor Mora
    Permalink to comment#

    The second demo worked on my mobile :D
    But it behaves weird when dragging an item up to their position. Also the animation of the slide you’re dragging works for the first time. But then it doesn’t.
    Here testing with chrome with an iPhone 5

  13. Yazin
    Permalink to comment#

    The second demo, David’s, works on my iPhone.

  14. Joshua Tenner
    Permalink to comment#

    Last example fails in Chrome.

    When moving an item to the last position, it does not re-numberthe elements.

  15. Permalink to comment#

    Very cool. I actually just built something very similar, only with Isotope and jQuery UI instead. The basic idea is you use the sortable’s start event to remove a dragged item from Isotope while it’s being moved, then add it back to Isotope on stop and reload the items (which animates them into the new order).

    No need to duplicate the items, and you also get to take advantage of Isotope’s other built-in features (like sorting and filtering, which Packery doesn’t do) ;-)

  16. Permalink to comment#

    HAML, SCSS and jQuery JS. I had to laugh so hard… We’re no longer writing instructions but rather descriptions of what we imagine.

  17. Zlatko
    Permalink to comment#

    I’ve worked with Alfresco which uses YUI stuff for this kind of behavior. I’m not sure on how it was implemented, but I know you had the drag and drop and this pretty animated stuff in it. It was YUI 2.6 or something, did you ever work with any of that stuff?

  18. Permalink to comment#

    I’ve managed to do it almost all by css transition (and still using jQuery sortable). During sorting, the basic html structure is like:

    <div class='slide'>slide1</div>
    <div class='sortable-placeholder'></div>
    <div class='slide ui-sortable-helper'>slide2</div>
    <div class='slide'>slide4</div>
    <div class='slide'>slide5</div>
    

    So there should be a transition of all slides after sortable-placeholder, but not include the ui-sortable-helper slide. The CSS looks like:

    .slide{  transition: all 0.3s ease; }
    .sortable-placeholder ~ .slide:not(.ui-sortable-helper){
        transform: translateY(60px);
    }
    

    This is the basic transition setting. But we need to cater for the sorting start/end that should have no transition. So I add some little js code to toggle class. This part is a bit tricky. And the result is not perfect.

    Here is the demo pen.

  19. xrosolar
    Permalink to comment#

    You ever get that feeling that everything easy is now hard, and everything hard is now impossible? That’s the sinking feeling I get in the pit of my stomach when I see these kinds of things. Or when I used to work with them.

    Brilliance, absolute brilliance, in the post and in the comments where people make a broken system dance. Absolutely without doubt.

    But isn’t life too short to be fighting so hard to do such a simple animation task?

    I will now go back to my compiler and desktop software and pray for all of your souls.

    (Tongue-in-cheek)

This comment thread is closed. If you have important information to share, you can always contact me.

*May or may not contain any actual "CSS" or "Tricks".