Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
    <div class="outer-container">
        <div class="group-container">
            <div class="sortable-container" data-sortable-container>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque bibendum commodo condimentum. Aenean leo lorem, ultrices nec facilisis vitae, convallis sit amet sapien. In vitae maximus libero. Pellentesque quis accumsan erat. Sed posuere maximus nisl, a ornare sem vulputate eu. Sed sit amet felis placerat turpis consectetur bibendum et quis tellus. Cras ac augue sit amet libero volutpat pharetra.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Vestibulum molestie odio ut purus scelerisque, ac tempor sem auctor. Cras ultricies tempus nulla nec fringilla. Etiam et libero et dui convallis luctus a ut lacus.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Pellentesque rutrum mattis turpis et aliquam. Donec leo est, imperdiet quis mi id, fringilla vehicula magna. Vivamus nec lacus vehicula, dapibus leo ut, dignissim urna. Ut quis vestibulum ante, lacinia feugiat nunc. Curabitur sit amet sem at est mollis scelerisque pharetra eu sem.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Nulla id arcu feugiat, tristique augue a, ornare nisi. Pellentesque mattis commodo vestibulum. In posuere luctus aliquam. Suspendisse potenti. Quisque facilisis molestie convallis. Integer posuere nunc ut enim consectetur sagittis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam tellus risus, viverra et erat sit amet, fringilla euismod augue. Duis molestie rutrum felis nec aliquet. In tempus blandit odio ut molestie. Nulla faucibus lobortis ligula, vel lacinia felis molestie sit amet. Nam eget nisi quis neque efficitur congue vel ut erat.</p>
                    </div>
                </div>
            </div>
        </div>
        <div class="group-container" >
            <div class="sortable-container" style="width: 70%;" data-sortable-container>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Vestibulum molestie odio ut purus scelerisque, ac tempor sem auctor. Cras ultricies tempus nulla nec fringilla. Etiam et libero et dui convallis luctus a ut lacus.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Pellentesque rutrum mattis turpis et aliquam. Donec leo est, imperdiet quis mi id, fringilla vehicula magna. Vivamus nec lacus vehicula, dapibus leo ut, dignissim urna. Ut quis vestibulum ante, lacinia feugiat nunc. Curabitur sit amet sem at est mollis scelerisque pharetra eu sem.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Nulla id arcu feugiat, tristique augue a, ornare nisi. Pellentesque mattis commodo vestibulum. In posuere luctus aliquam. Suspendisse potenti. Quisque facilisis molestie convallis. Integer posuere nunc ut enim consectetur sagittis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam tellus risus, viverra et erat sit amet, fringilla euismod augue. Duis molestie rutrum felis nec aliquet. In tempus blandit odio ut molestie. Nulla faucibus lobortis ligula, vel lacinia felis molestie sit amet. Nam eget nisi quis neque efficitur congue vel ut erat.</p>
                    </div>
                </div>
            </div>
            <div class="sortable-container" style="width: 30%;" data-sortable-container>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque bibendum commodo condimentum. Aenean leo lorem, ultrices nec facilisis vitae, convallis sit amet sapien. In vitae maximus libero. Pellentesque quis accumsan erat. Sed posuere maximus nisl, a ornare sem vulputate eu. Sed sit amet felis placerat turpis consectetur bibendum et quis tellus. Cras ac augue sit amet libero volutpat pharetra.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Vestibulum molestie odio ut purus scelerisque, ac tempor sem auctor. Cras ultricies tempus nulla nec fringilla. Etiam et libero et dui convallis luctus a ut lacus.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Pellentesque rutrum mattis turpis et aliquam. Donec leo est, imperdiet quis mi id, fringilla vehicula magna. Vivamus nec lacus vehicula, dapibus leo ut, dignissim urna. Ut quis vestibulum ante, lacinia feugiat nunc. Curabitur sit amet sem at est mollis scelerisque pharetra eu sem.</p>
                    </div>
                </div>
                <div class="sortable" data-sortable>
                    <div class="sortable-header">
                        <div class="sortable-trigger" data-sortable-trigger></div>
                        <h2 class="sortable-title">Component Title</h2>
                    </div>
                    <div class="sortable-body">
                        <p>Nulla id arcu feugiat, tristique augue a, ornare nisi. Pellentesque mattis commodo vestibulum. In posuere luctus aliquam. Suspendisse potenti. Quisque facilisis molestie convallis. Integer posuere nunc ut enim consectetur sagittis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam tellus risus, viverra et erat sit amet, fringilla euismod augue. Duis molestie rutrum felis nec aliquet. In tempus blandit odio ut molestie. Nulla faucibus lobortis ligula, vel lacinia felis molestie sit amet. Nam eget nisi quis neque efficitur congue vel ut erat.</p>
                    </div>
                </div>
            </div>
        </div>
    </div>


              
            
!

CSS

              
                body {
  display: flex;
  justify-content: center;
  padding: 20px;
  background: white;
}

.outer-container {
  width: 1200px;
  margin: 0px auto;
}

.group-container {
  display: flex;
  width: 100%;
  gap: 10px;
  margin-bottom: 10px;
  justify-content: space-between;
}

.sortable-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  min-width: 100px;
  row-gap: 10px;
}

.sortable {
  box-sizing: border-box;
  width: 100%;
  background: lightblue;
}

.sortable-header {
  width: 100%;
  height: 50px;
  padding: 0px;
  background: rgb(56, 194, 240);
}

.sortable-trigger {
  display: inline-block;
  width: 50px;
  height: 100%;
  background: rgb(2, 119, 165);
  cursor: pointer;
}

.sortable-title {
  display: inline-block;
  margin: 0px;
  padding: 0px 0px 0px 10px;
  color: black;
  vertical-align: top;
  line-height: 2;
}

.sortable-body {
  padding: 10px 20px;
}

              
            
!

JS

              
                
gsap.registerPlugin(Draggable, Flip);

let sortableElement; //the element that's currently being dragged around
let placeholderElement; //the element that subs in for the draggable element
let closestElement; //the element nearest to the dragged element
let sortablesSelector = '[data-sortable]';
let sortablesTriggerSelector = '[data-sortable-trigger]';
let isScrollingTimeout = false; //to prevent sorting when moving a sortable up/down the window
let isScrolling = false;
let isFlipping = false;  //let the shift finish first
let startDragPoint = { left: 0, top: 0 }; //to prevent rapid fire position changes, have to move a minimum amount prior to another change


/**
 * Don't do sorting while moving up or down the window due to dragging at the window edges 
 */
window.addEventListener('scroll', (event) => {
    isScrolling = true;
    window.clearTimeout(isScrollingTimeout); // Clear our timeout throughout the scroll
    isScrollingTimeout = setTimeout(() => { // Set a timeout to run after scrolling ends
        isScrolling = false; // Run the callback
    }, 66);
}, false);


/**
 * Fire up Draggable, adding sorting functionality while dragging
 */
const initSortables = () => {
    document.querySelectorAll(sortablesSelector).forEach((item) => {
        Draggable.create(item, {
            type: 'x, y',
            trigger: item.querySelector(sortablesTriggerSelector),
            autoScroll: 1,
            onDragStart: (event) => onDragStart(event),
            onDrag: () => onDrag(),
            onDragEnd: () => onDragEnd(),
        });
    });
}

/**
 * To be able to sort elements and snap back to correct position
 */
 const updateSortables = () => {
    document.querySelectorAll(sortablesSelector).forEach((item, index) => {
        item.dataset.currentIndex = index; //used to compare items
        item.dataset.originalLeft = item.getBoundingClientRect().left;
        item.dataset.leftDifference = 0;
        item.dataset.originalTop = item.getBoundingClientRect().top + window.scrollY;
        item.dataset.topDifference = 0;
    });
}

/**
 * Sub in a placeholder to use in place of the element being dragged, 
 * which gets pulled out so it can go into any other parent container 
 * that contains the sortablesSelector
 */
const onDragStart = (event) => {
    sortableElement = event.target.closest(sortablesSelector); //use the parent container regardless of selection
    placeholderElement = getPlaceholderElement(sortableElement);
    startDragPoint = updateDragPoint(); //to track when we can allow sorting check based on MIN_MOVEMENT
    document.body.style.overflowX = 'hidden'; //avoid horizontal scroll bars
    placeholderElement.style.opacity = 0.2; //displayed as just a space that the sortable occupies
    pullDraggedElementOut(sortableElement);
}

/**
 * Create a clone of the element being dragged to use as a placeholder 
 * and to keep it in position as the actually dragged element will be pulled out
 * 
 * @param { DOMElement }  
 * 
 * @returns { DOMElement }  
 */
const getPlaceholderElement = (element) => {
    const clone = element.cloneNode(true);
    element.parentNode.insertBefore(clone, element);
    return clone;
}

/**
 * To track drag movement
 * 
 * @returns { left, top } 
 */
const updateDragPoint = () => {
    return { 
        left: sortableElement.getBoundingClientRect().left,
        top: sortableElement.getBoundingClientRect().top + window.scrollY
    }
}

/**
 * With the placeholder element in place, the dragged element is pulled 
 * out so it doesn't impact our ability to sort through the items
 * 
 * @param { DOMElement }  
 */
const pullDraggedElementOut = (element) => {
    document.body.appendChild(element);
    element.style.position = 'absolute';
    element.style.top = `${placeholderElement.getBoundingClientRect().top + window.scrollY}px`;
    element.style.left = `${placeholderElement.getBoundingClientRect().left}px`;
    element.style.width = `${placeholderElement.getBoundingClientRect().width}px`;
    element.style.boxShadow = '0px 0px 5px #888888';
    element.removeAttribute('data-sortable'); //so there's no position checks when dragging, as the placeholder is there instead
}

/**
 * Constantly check for the nearest other sortable and update position 
 * based on whether a move is valid
 */
 const onDrag = () => {
    if (canCheckForChange()) { //don't be checking while scrolling, as can jar the window and produce weird behaviour
        const sortablesToCheckList = Array.from(document.querySelectorAll(sortablesSelector)); //grab all sortable elements 
        closestElement = getClosestElement(getElementsLeftAndMidHeightPoint(sortableElement), sortablesToCheckList);
        if (canShiftDown(sortableElement, closestElement)) {
            flipState(sortableElement, closestElement, true);
        } else if (canShiftUp(sortableElement, closestElement)) {
            flipState(sortableElement, closestElement, false);
        }
    }
}

/**
 *  Don't be checking while have hardly dragged, or while the window is scrolling 
 * 
 * @returns { Boolean } 
 */
const canCheckForChange = () => {
    const currentDragPoint = { 
        left: sortableElement.getBoundingClientRect().left,
        top: sortableElement.getBoundingClientRect().top + window.scrollY
    }
    const distance = Math.sqrt(Math.pow(startDragPoint.left - currentDragPoint.left, 2) + Math.pow(startDragPoint.top - currentDragPoint.top, 2));
    return distance > 50 && !isScrolling;
}

/**
 * Find the closest element to the element being dragged, which can be the dragged element
 * itself if haven't dragged very far (or the items are very tall)
 * 
 * @param { x, y }  
 * @param { NodeList }  
 * 
 * @returns { DOMElement }   
 */
const getClosestElement = (targetPoint, elements) => {
    let closestElement = elements[0];
    let minDistance = 99999; //if this doesn't get overwritten we got issues...
    elements.forEach((element) => { //loop through each element and find the closest element using pythagoras theorem
        const elementsMidPoint = getElementsLeftAndMidHeightPoint(element);
        const distance = Math.sqrt(Math.pow(elementsMidPoint.left - targetPoint.left, 2) + Math.pow(elementsMidPoint.top - targetPoint.top, 2));
        if (distance < minDistance) {
            minDistance = distance;
            closestElement = element;
        }
    });
    return closestElement;
};

/**
 * To get an elements left and mid height point relative to the document
 * 
 * @param { DOMElement }  
 * 
 * @returns { x, y }   
 */
const getElementsLeftAndMidHeightPoint = (element) => {
    return {
        left: element.getBoundingClientRect().left,
        top: (element.getBoundingClientRect().top + window.scrollY) + (element.offsetHeight / 2)
    };
}

/**
 * If the midpoint of the dragged element is below the midpoint of the element below
 * 
 * @param { DOMElement }  
 * @param { DOMElement }  
 * 
 * @returns { Boolean }  
 */
const canShiftDown = (draggedElement, closestElement) => {
    return (getElementsMidPoint(draggedElement).top > getElementsMidPoint(closestElement).top);
}

/**
 * If the midpoint of the dragged element is above the midpoint of the element above
 * 
 * @param { DOMElement }  
 * @param { DOMElement }  
 * 
 * @returns { Boolean }  
 */
const canShiftUp = (draggedElement, closestElement) => {
    return (getElementsMidPoint(draggedElement).top < getElementsMidPoint(closestElement).top);
}

/**
 * To get an elements mid point relative to the document
 * 
 * @param { DOMElement }  
 * 
 * @returns { x, y }   
 */
const getElementsMidPoint = (element) => {
    return {
        left: element.getBoundingClientRect().left + (element.offsetWidth / 2),
        top: (element.getBoundingClientRect().top + window.scrollY) + (element.offsetHeight / 2)
    };
}

/**
 * Use Flip to shift the elements around, recording the difference in shift 
 * for when the dragged element is released
 * 
 * @param { DOMElement }  
 * @param { DOMElement }  
 * @param { Boolean }  
 */
const flipState = (draggedElement, closestElement, isShiftingDown) => {
    if (!isFlipping) {
        isFlipping = true;
        startDragPoint = updateDragPoint();
        const state = Flip.getState(document.querySelectorAll(sortablesSelector));
        if (isShiftingDown) {
            insertAfter(closestElement, placeholderElement);
        } else {
            insertBefore(closestElement, placeholderElement);
        }
        draggedElement.dataset.leftDifference = placeholderElement.getBoundingClientRect().left - parseInt(placeholderElement.dataset.originalLeft);
        draggedElement.dataset.topDifference = (placeholderElement.getBoundingClientRect().top + window.scrollY) - parseInt(placeholderElement.dataset.originalTop);
        gsap.to(draggedElement, { width: placeholderElement.parentElement.offsetWidth, duration: 0.5 } ); 
        document.querySelectorAll(sortablesSelector).forEach((item, index) => {
            item.dataset.currentIndex = index;
        });
        Flip.from(state, {
            duration: 0.3,
            onComplete: () => {
                isFlipping = false;
                onDrag(); //check after Flip to see if another is required
            }
        });
    }
}

/**
 * Insert the target node before the reference
 * 
 * @param { DOMElement }  
 * @param { DOMElement }  
 */
const insertBefore = (referenceNode, targetNode) => {
  referenceNode.parentNode.insertBefore(targetNode, referenceNode);
}

/**
 * Insert the target node after the reference
 * 
 * @param { DOMElement }  
 * @param { DOMElement }  
 */
const insertAfter = (referenceNode, targetNode) => {
  referenceNode.parentNode.insertBefore(targetNode, referenceNode.nextSibling);
}

/**
 * Just snap back to where it now belongs
 */
const onDragEnd = () => {
    snapElementBack(sortableElement);
}

/**
 * Snap the dragged element back to its original OR new position
 * and then remove the placeholder element once complete, 
 * setting up for another drag
 * 
 * @param { DOMElement }  
 */
const snapElementBack = (element) => {
    gsap.to(element, {
        x: 0 + parseInt(element.dataset.leftDifference), //to accommodate for potential position changes
        y: 0 + parseInt(element.dataset.topDifference),
        width: placeholderElement.parentElement.offsetWidth, //parentElement as the placeHolder may not have fully shifted into place before mouse release
        duration: 0.3,
        overwrite: true,
        onComplete: () => {
            insertAfter(placeholderElement, element);
            gsap.set(element, { x: 0, y: 0, width: '100%' });
            element.style.position = 'static';
            element.style.boxShadow = 'none';
            element.setAttribute('data-sortable', '');
            document.body.style.overflowX = 'auto';
            placeholderElement.remove();
            updateSortables();
    } });
}

initSortables();
updateSortables();
              
            
!
999px

Console