Skip to content

Sortable

<quiet-sortable> stable since 4.0

Enables drag and drop sorting of arbitrary elements.

Sortable enables drag-and-drop reordering of its direct child elements using mouse, touch, and keyboard inputs. As an item is dragged, its siblings smoothly shift to make room at the new position. Every direct child element is sortable by default.

Sir Fluffington III
Sir Fluffington III
Professional napper and treat connoisseur
Meowy McGee
Meowy McGee
Freedom's just another word for nothing left to lose
Whisker Doodle
Whisker Doodle
Part-time philosopher, full-time lap cat
Chairman Meow
Chairman Meow
Demands belly rubs on his own terms
Purrlock Holmes
Purrlock Holmes
Solves the mystery of the missing treats
<quiet-sortable id="sortable__basic">
  <div class="item">
    <img alt="Sir Fluffington III" src="https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?q=80&w=256&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D">
    <div>
      <div class="name">Sir Fluffington III</div>
      <div class="description">Professional napper and treat connoisseur</div>
    </div>
  </div>
  <div class="item">
    <img alt="Meowy McGee" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?q=80&w=256&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D">
    <div>
      <div class="name">Meowy McGee</div>
      <div class="description">Freedom's just another word for nothing left to lose</div>
    </div>
  </div>
  <div class="item">
    <img alt="Whisker Doodle" src="https://images.unsplash.com/photo-1574158622682-e40e69881006?q=80&w=256&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D">
    <div>
      <div class="name">Whisker Doodle</div>
      <div class="description">Part-time philosopher, full-time lap cat</div>
    </div>
  </div>
  <div class="item">
    <img alt="Chairman Meow" src="https://images.unsplash.com/photo-1569591159212-b02ea8a9f239?q=80&w=256&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D">
    <div>
      <div class="name">Chairman Meow</div>
      <div class="description">Demands belly rubs on his own terms</div>
    </div>
  </div>
  <div class="item">
    <img alt="Purrlock Holmes" src="https://images.unsplash.com/photo-1526336024174-e58f5cdd8e13?q=80&w=256&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D">
    <div>
      <div class="name">Purrlock Holmes</div>
      <div class="description">Solves the mystery of the missing treats</div>
    </div>
  </div>
</quiet-sortable>

<style>
  #sortable__basic {
    gap: 0.5rem;

    .item {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }

    img {
      flex: 0 0 auto;
      width: 48px;
      height: 48px;
      border-radius: var(--quiet-border-radius-circle);
      object-fit: cover;
    }

    .name {
      font-weight: var(--quiet-font-weight-semibold);
    }

    .description {
      font-size: 0.875rem;
      color: var(--quiet-text-muted);
    }
  }
</style>

Examples Jump to heading

Using handles Jump to heading

By default, the entire item is draggable. To restrict dragging to a specific element, set the handles attribute to a CSS selector that matches the handle elements within each item. Any valid CSS selector works with the handles attribute, for example handles=".grip" will target a handle with class="grip" and handles="quiet-icon" will target <quiet-icon> elements.

Handle elements must be inside the sortable item. Elements matching the selector that exist outside of an item will be ignored. If multiple elements within an item match the selector, clicking any of them will start a drag.

Feed the cats
Change the litter box
Buy catnip
Schedule vet appointment
Playtime
<quiet-sortable handles=".handle" id="sortable__handles">
  <div class="item">
    <quiet-icon class="handle" name="grip-vertical"></quiet-icon>
    <span>Feed the cats</span>
  </div>
  <div class="item">
    <quiet-icon class="handle" name="grip-vertical"></quiet-icon>
    <span>Change the litter box</span>
  </div>
  <div class="item">
    <quiet-icon class="handle" name="grip-vertical"></quiet-icon>
    <span>Buy catnip</span>
  </div>
  <div class="item">
    <quiet-icon class="handle" name="grip-vertical"></quiet-icon>
    <span>Schedule vet appointment</span>
  </div>
  <div class="item">
    <quiet-icon class="handle" name="grip-vertical"></quiet-icon>
    <span>Playtime</span>
  </div>
</quiet-sortable>

<style>
  #sortable__handles {
    gap: 0.5rem;

    .item {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);

      quiet-icon {
        font-size: 1.25em;
      }
    }

    .handle {
      cursor: grab;
      color: var(--quiet-text-muted);

      &:active {
        cursor: grabbing;
      }
    }
  }
</style>

When handles are set, the container's default grab cursor is suppressed. You should add your own cursor: grab styles to handle elements so users know where to click to drag.

For more advanced handle logic, you can set the handles property to a callback function instead. The function receives each element in the event path and the direct child item, and should return true for elements that are valid drag handles.

sortable.handles = (el, item) => {
  return el.classList.contains('grip');
};

Grouping Jump to heading

Items can be dragged between sortable containers that share the same group attribute. This is useful for kanban-style boards and similar interfaces.

To Do

Research competitors
Write specifications
Design mockups

Done

Set up repository
Create project plan
<div id="sortable__grouping">
  <div class="column">
    <h4>To Do</h4>
    <quiet-sortable group="tasks">
      <div class="item">Research competitors</div>
      <div class="item">Write specifications</div>
      <div class="item">Design mockups</div>
    </quiet-sortable>
  </div>

  <div class="column">
    <h4>Done</h4>
    <quiet-sortable group="tasks">
      <div class="item">Set up repository</div>
      <div class="item">Create project plan</div>
    </quiet-sortable>
  </div>
</div>

<style>
  #sortable__grouping {
    display: flex;
    gap: 2rem;

    @media (max-width: 480px) {
      flex-direction: column;
    }

    .column {
      flex: 1 1 0;

      h4 {
        margin: 0 0 0.75rem;
      }
    }

    .column + .column {
      border-left: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      padding-left: 2rem;

      @media (max-width: 480px) {
        border-left: none;
        padding-left: 0;
        border-top: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
        padding-top: 2rem;
      }
    }

    quiet-sortable {
      gap: 0.5rem;
      min-height: 100px;
      border-radius: var(--quiet-border-radius-xs);
      outline-offset: 0.5rem;
    }

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

When using groups, it's a good idea to set a min-height on each container so it remains a visible drop target even when all of its items have been dragged out.

Drop validation Jump to heading

When using groups, you can control which containers accept which items by setting the accepts property on the target container. The callback receives the dragged item and the source container, and should return true to accept or false to reject.

Containers that reject the item receive the drop-invalid CSS state at drag start, letting you style them to indicate they won't accept the item. Containers that accept receive the drop-target state.

sortable.accepts = (item, sourceContainer) => {
  return item.dataset.priority === 'high';
};

In this example, the second container only accepts high-priority items.

All Tasks

High priority task
Low priority task
Another high priority
Another low priority

High Priority Only

<div id="sortable__accepts">
  <div class="column">
    <h4>All Tasks</h4>
    <quiet-sortable group="validated" id="sortable__accepts-all">
      <div class="item" data-priority="high">High priority task</div>
      <div class="item" data-priority="low">Low priority task</div>
      <div class="item" data-priority="high">Another high priority</div>
      <div class="item" data-priority="low">Another low priority</div>
    </quiet-sortable>
  </div>

  <div class="column">
    <h4>High Priority Only</h4>
    <quiet-sortable group="validated" id="sortable__accepts-high">
    </quiet-sortable>
  </div>
</div>

<script>
  const highOnly = document.getElementById('sortable__accepts-high');

  highOnly.accepts = (item, sourceContainer) => {
    return item.dataset.priority === 'high';
  };
</script>

<style>
  #sortable__accepts {
    display: flex;
    gap: 2rem;

    @media (max-width: 480px) {
      flex-direction: column;
    }

    .column {
      flex: 1 1 0;

      h4 {
        margin: 0 0 0.75rem;
      }
    }

    .column + .column {
      border-left: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      padding-left: 2rem;

      @media (max-width: 480px) {
        border-left: none;
        padding-left: 0;
        border-top: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
        padding-top: 2rem;
      }
    }

    quiet-sortable {
      gap: 0.5rem;
      min-height: 100px;
      border-radius: var(--quiet-border-radius-md);
      outline-offset: 0.25rem;
    }

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border-radius: var(--quiet-border-radius-md);
    }

    .item[data-priority="low"] {
      background: var(--quiet-primary-fill-soft);
      color: var(--quiet-primary-text-on-soft);
    }

    .item[data-priority="high"] {
      background: var(--quiet-destructive-fill-soft);
      color: var(--quiet-destructive-text-on-soft);
    }
  }
</style>

Filtering items Jump to heading

Use the filter property to control which items can be dragged. Set it to a function that receives the item element and returns true to allow dragging or false to prevent it.

sortable.filter = (item) => {
  return !item.classList.contains('locked');
};

In this example, items with the locked class cannot be dragged.

Draggable item
Locked item
Draggable item
Locked item
Draggable item
<quiet-sortable id="sortable__can-drag">
  <div class="item">Draggable item</div>
  <div class="item locked">Locked item</div>
  <div class="item">Draggable item</div>
  <div class="item locked">Locked item</div>
  <div class="item">Draggable item</div>
</quiet-sortable>

<script>
  const filterSortable = document.getElementById('sortable__can-drag');

  filterSortable.filter = (item) => {
    return !item.classList.contains('locked');
  };
</script>

<style>
  #sortable__can-drag {
    gap: 0.5rem;

    .item {
      display: flex;
      align-items: center;
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);

      &.locked {
        opacity: 0.5;
        cursor: not-allowed;
      }
    }
  }
</style>

It's usually a good idea to apply cursor: not-allowed to locked items via CSS.

Scrollable containers Jump to heading

When dragging items inside a scrollable container, the container will automatically scroll as the pointer approaches its edges. The closer the pointer gets to an edge, the faster it scrolls.

Astronomy
Biology
Chemistry
Dentistry
Engineering
Forestry
Geography
History
Immunology
Journalism
Kinesiology
Linguistics
<quiet-sortable id="sortable__scrollable">
  <div class="item">Astronomy</div>
  <div class="item">Biology</div>
  <div class="item">Chemistry</div>
  <div class="item">Dentistry</div>
  <div class="item">Engineering</div>
  <div class="item">Forestry</div>
  <div class="item">Geography</div>
  <div class="item">History</div>
  <div class="item">Immunology</div>
  <div class="item">Journalism</div>
  <div class="item">Kinesiology</div>
  <div class="item">Linguistics</div>
</quiet-sortable>

<style>
  #sortable__scrollable {
    max-height: 200px;
    overflow-y: auto;
    gap: 0.5rem;
    padding: 1rem;
    margin: -1rem;
    background-color: var(--quiet-neutral-fill-softest);
    border-radius: var(--quiet-border-radius-md);

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Copying items Jump to heading

By default, dragging an item between grouped containers moves it. To copy items instead, set the copy property on the source container. This callback receives the dragged item and returns a new element to insert at the drop position. Return a falsy value to fall back to normal move behavior.

sortable.copy = (item) => {
  return item.cloneNode(true);
};

The source item never leaves its container. A visual ghost follows the pointer and the copy animates into place on drop. The source container is inert during a copy drag, so its items won't rearrange and dropping back on it cancels the drag.

Since you control what the callback returns, you can return any element: a modified clone or a completely different element.

sortable.copy = (item) => {
  const el = document.createElement('div');
  el.className = 'card';
  el.textContent = `Copy of ${item.textContent}`;
  el.dataset.sourceId = item.id;
  return el;
};

Try dragging items from the components palette into the page.

Components

Button
Input
Card

Page

<div id="sortable__copy">
  <div class="column">
    <h4>Components</h4>
    <quiet-sortable group="builder" id="sortable__palette">
      <div class="item">Button</div>
      <div class="item">Input</div>
      <div class="item">Card</div>
    </quiet-sortable>
  </div>

  <div class="column">
    <h4>Page</h4>
    <quiet-sortable group="builder" id="sortable__canvas">
    </quiet-sortable>
  </div>
</div>

<script>
  const copyPalette = document.getElementById('sortable__palette');
  let copyCount = 1;

  copyPalette.copy = (item) => {
    const clone = item.cloneNode(true);
    clone.textContent = `${item.textContent} ${copyCount++}`;
    return clone;
  };
</script>

<style>
  #sortable__copy {
    display: flex;
    gap: 2rem;

    .column {
      flex: 1 1 0;

      h4 {
        margin: 0 0 0.75rem;
      }
    }

    quiet-sortable {
      gap: 0.5rem;
      min-height: 100px;
      padding: 0.75rem;
      background-color: var(--quiet-neutral-fill-softest);
      border-radius: var(--quiet-border-radius-md);
    }

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Horizontal layout Jump to heading

Apply flex-direction: row to the sortable container for horizontal sorting.

1
2
3
4
5
<quiet-sortable id="sortable__horizontal">
  <div class="box" style="background-color: #f05655;">1</div>
  <div class="box" style="background-color: #f0803a;">2</div>
  <div class="box" style="background-color: #5dbb56;">3</div>
  <div class="box" style="background-color: #4b97f4;">4</div>
  <div class="box" style="background-color: #937cfb;">5</div>
</quiet-sortable>

<style>
  #sortable__horizontal {
    flex-direction: row;
    gap: 1rem;

    .box {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 80px;
      height: 80px;
      border-radius: var(--quiet-border-radius-md);
      color: white;
      font-size: 1.25rem;
      font-weight: var(--quiet-font-weight-semibold);
    }
  }
</style>

Horizontal flex layout Jump to heading

To arrange sortable items horizontally with wrapping, apply flex-direction: row and flex-wrap: wrap to the host element.

Tabby Calico Siamese Persian Maine Coon Sphynx
<quiet-sortable id="sortable__horizontal-flex">
  <span class="tag" style="background-color: #f05655;">Tabby</span>
  <span class="tag" style="background-color: #f0803a;">Calico</span>
  <span class="tag" style="background-color: #e89b25;">Siamese</span>
  <span class="tag" style="background-color: #48b873;">Persian</span>
  <span class="tag" style="background-color: #4b97f4;">Maine Coon</span>
  <span class="tag" style="background-color: #ae76f6;">Sphynx</span>
</quiet-sortable>

<style>
  #sortable__horizontal-flex {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.5rem;

    .tag {
      padding: 0.5rem 1rem;
      border-radius: var(--quiet-border-radius-pill);
      color: white;
      font-weight: var(--quiet-font-weight-semibold);
    }
  }
</style>

Grid layout Jump to heading

Sortable works with CSS grid layouts as well.

1
2
3
4
5
6
7
8
9
<quiet-sortable id="sortable__grid">
  <div class="tile" style="background-color: #f05655;">1</div>
  <div class="tile" style="background-color: #f0803a;">2</div>
  <div class="tile" style="background-color: #e89b25;">3</div>
  <div class="tile" style="background-color: #5dbb56;">4</div>
  <div class="tile" style="background-color: #48b873;">5</div>
  <div class="tile" style="background-color: #20b9bd;">6</div>
  <div class="tile" style="background-color: #4b97f4;">7</div>
  <div class="tile" style="background-color: #937cfb;">8</div>
  <div class="tile" style="background-color: #e468b0;">9</div>
</quiet-sortable>

<style>
  #sortable__grid {
    display: grid;
    grid-template-columns: repeat(3, 100px);

    @media (max-width: 480px) {
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
    }
    gap: 1rem;

    .tile {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 80px;
      border-radius: var(--quiet-border-radius-md);
      color: white;
      font-size: 1.25rem;
      font-weight: var(--quiet-font-weight-semibold);
    }
  }
</style>

Block layout Jump to heading

Block containers work as expected. The component defaults to a column flex layout, but you can set display: block on the host element to use block layout instead.

Item 1
Item 2
Item 3
Item 4
<quiet-sortable id="sortable__block">
  <div class="item">Item 1</div>
  <div class="item">Item 2</div>
  <div class="item">Item 3</div>
  <div class="item">Item 4</div>
</quiet-sortable>

<style>
  #sortable__block {
    display: block;

    .item {
      padding: 0.75rem 1rem;
      margin-block-end: 0.5rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Custom animation Jump to heading

Set the --duration and --easing custom properties to control the rearrangement animation. The default duration is 0.15s with an ease timing function.

Slow and bouncy
Try reordering
These items
To see the effect
<quiet-sortable id="sortable__animation" style="--duration: 0.5s; --easing: cubic-bezier(0.34, 1.56, 0.64, 1);">
  <div class="item">Slow and bouncy</div>
  <div class="item">Try reordering</div>
  <div class="item">These items</div>
  <div class="item">To see the effect</div>
</quiet-sortable>

<style>
  #sortable__animation {
    gap: 0.5rem;

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Disabling animations Jump to heading

Add the without-animation attribute to disable all rearrangement animations. Items will still be dragged and reordered, but siblings won't animate into their new positions.

No animation
Items still reorder
But without transitions
Try dragging these
<quiet-sortable without-animation id="sortable__without-animation">
  <div class="item">No animation</div>
  <div class="item">Items still reorder</div>
  <div class="item">But without transitions</div>
  <div class="item">Try dragging these</div>
</quiet-sortable>

<style>
  #sortable__without-animation {
    gap: 0.5rem;

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Disabled Jump to heading

Add the disabled attribute to prevent sorting.

Can't drag me
Or me
Or me either
<quiet-sortable disabled id="sortable__disabled">
  <div class="item">Can't drag me</div>
  <div class="item">Or me</div>
  <div class="item">Or me either</div>
</quiet-sortable>

<style>
  #sortable__disabled {
    gap: 0.5rem;

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Listening to events Jump to heading

The quiet-sort-start event is dispatched when a drag begins. You can call event.preventDefault() to prevent it. As the item is dragged, quiet-sort-move is dispatched each time it moves to a new position.

When an item is dropped, quiet-before-sort-end is dispatched before the move is finalized and is cancelable. Call event.preventDefault() to prevent the move and return the item to its source. After the drop animation completes, quiet-sort-end is dispatched as confirmation.

If the drag is canceled via Esc, dropping outside a container, or calling cancel() programmatically, the item returns to its source and quiet-sort-cancel is dispatched.

Item 1
Item 2
Item 3
Item 4
Try dragging an item and check the console for events.
<div id="sortable__events">
  <quiet-sortable>
    <div class="item">Item 1</div>
    <div class="item">Item 2</div>
    <div class="item">Item 3</div>
    <div class="item">Item 4</div>
  </quiet-sortable>

  <p><small>Try dragging an item and check the console for events.<small></p>
</div>

<script>
  const eventsContainer = document.getElementById('sortable__events');
  const eventsSortable = eventsContainer.querySelector('quiet-sortable');

  eventsSortable.addEventListener('quiet-sort-start', event => {
    console.log('sort-start:', event.detail);
  });

  eventsSortable.addEventListener('quiet-sort-move', event => {
    console.log('sort-move:', event.detail);
  });

  eventsSortable.addEventListener('quiet-before-sort-end', event => {
    console.log('before-sort-end:', event.detail);
  });

  eventsSortable.addEventListener('quiet-sort-end', event => {
    console.log('sort-end:', event.detail);
  });

  eventsSortable.addEventListener('quiet-sort-cancel', event => {
    console.log('sort-cancel:', event.detail);
  });
</script>

<style>
  #sortable__events {
    quiet-sortable {
      gap: 0.5rem;
      margin-block-end: 0.5rem;
    }

    .item {
      padding: 0.75rem 1rem;
      background-color: var(--quiet-paper-color);
      border: var(--quiet-border-style) var(--quiet-border-width) var(--quiet-neutral-stroke-softer);
      border-radius: var(--quiet-border-radius-md);
      box-shadow: var(--quiet-shadow-softer);
      color: var(--quiet-neutral-text-on-soft);
    }
  }
</style>

Keyboard support Jump to heading

Items can be reordered using the keyboard. Use Tab to focus an item, then press Space or Enter to grab it. While grabbed, use to move the item, then press Space or Enter to drop it in place. Press Escape to cancel and return the item to its original position.

Key Not grabbed Grabbed
Focus previous item Move item up
Focus next item Move item down
Home Focus first item Move item to first position
End Focus last item Move item to last position
Space / Enter Grab focused item Drop item
Escape Cancel reorder

The component announces actions to screen readers via a live region. When an item is grabbed, moved, dropped, or canceled, a description of the action and the item's position is announced.

To style the grabbed item, use the data-grabbed attribute which is applied to the item while it's being reordered via keyboard.

quiet-sortable [data-grabbed] {
  outline: solid 2px var(--quiet-focus-color);
  outline-offset: -2px;
}

Keyboard reordering operates within a single group. Cross-container keyboard moves (e.g., between grouped sortable containers) are not currently supported.

API Jump to heading

Importing Jump to heading

The autoloader is the recommended way to import components but, if you prefer to do it manually, the following code snippets will be helpful.

CDN Self-hosted

To manually import <quiet-sortable> from the CDN, use the following code.

import 'https://cdn.quietui.org/v4.0.0/components/sortable/sortable.js';

To manually import <quiet-sortable> from a self-hosted distribution, use the following code. Remember to replace /path/to/quiet with the appropriate local path.

import '/path/to/quiet/components/sortable/sortable.js';

Slots Jump to heading

Sortable supports the following slots. Learn more about using slots

Name Description
(default) The elements to sort. Every direct child will be sortable.

Properties Jump to heading

Sortable has the following properties that can be set with corresponding attributes. In many cases, the attribute's name is the same as the property's name. If an attribute is different, it will be displayed after the property. Learn more about attributes and properties

Property Description Reflects Type Default
disabled Disables sorting. When disabled, items cannot be dragged. boolean false
withoutAnimation
without-animation
Disables all drag and drop animations when present. Items will still rearrange, but without animated transitions. boolean false
group An optional group name. Sortable containers with the same group allow items to be dragged between them. string ''
handles An optional handle selector or callback that restricts which elements can initiate a drag. When set to a CSS selector string, only clicks originating from matching elements within an item will start a drag. When set to a callback, the function receives each element in the event path and the direct child item, and should return true for elements that are valid drag handles. string
((el: HTMLElement, item: HTMLElement) => boolean)
''
dragThreshold
drag-threshold
The distance in pixels the pointer must move before a drag starts. number 5
scrollEdgeSize
scroll-edge-size
The size of the edge zone in pixels where auto-scrolling activates during drag. number 40
scrollMaxSpeed
scroll-max-speed
The maximum scroll speed in pixels per frame during auto-scrolling. number 15
filter An optional callback that determines whether a specific child element can be dragged. Return false to prevent the item from being dragged. Property only. ((item: HTMLElement) => boolean)
null
null
accepts An optional callback set on a target container that determines whether it will accept a specific dragged item. Return false to reject the item. Evaluated at drag start for all grouped containers. Property only. ((item: HTMLElement, sourceContainer: QuietSortable) => boolean)
null
null
copy An optional callback that enables copy-on-drag behavior. Called when a drag starts. Return an element to use as the copy, or return a falsy value to fall back to normal move behavior. The source container is inert during a copy drag — its items won't rearrange and it won't accept drops from other containers. Property only. ((item: HTMLElement) => HTMLElement
null
undefined)
null
null

Methods Jump to heading

Sortable supports the following methods. You can obtain a reference to the element and call them like functions in JavaScript. Learn more about methods

Name Description Arguments
cancel() Programmatically cancels the current drag, returning the item to its original position with animation.

Events Jump to heading

Sortable dispatches the following custom events. You can listen to them the same way was native events. Learn more about custom events

Name Description
quiet-sort-start Emitted when a drag starts. Cancel this event to prevent dragging.
quiet-sort-move Emitted when the dragged item moves to a new position.
quiet-before-sort-end Emitted immediately when the user drops an item, before the drop animation. Cancel this event to prevent the move and return the item to its source position.
quiet-sort-end Emitted after the drop animation completes. This is a confirmation event.
quiet-sort-cancel Emitted when a drag is canceled, after the item is returned to its source position.

CSS custom properties Jump to heading

Sortable supports the following CSS custom properties. You can style them like any other CSS property. Learn more about CSS custom properties

Name Description Default
--duration The duration of the rearrangement animation. 0.15s
--easing The easing function for the rearrangement animation. ease

Custom States Jump to heading

Sortable has the following custom states. You can target them with CSS using the selectors shown below. Learn more about custom states

Name Description CSS selector
disabled Applied when the component is disabled. :state(disabled)
sorting Applied when a drag is in progress. :state(sorting)
drag-source Applied to the source container for the entire duration of a drag. :state(drag-source)
drop-target Applied to compatible grouped containers that accept the dragged item. :state(drop-target)
drop-invalid Applied to compatible grouped containers that rejected the dragged item via accepts. :state(drop-invalid)
drag-over Applied to a grouped container when an item from another container is dragged over it. :state(drag-over)
copy-source Applied to the source container during a copy drag. :state(copy-source)
has-handles Applied when the handles property is set. :state(has-handles)
keyboard-reordering Applied when an item is grabbed via keyboard and is being reordered. :state(keyboard-reordering)
Search this website Toggle dark mode View the code on GitHub Follow @quietui.org on Bluesky Follow @quiet_ui on X

    No results found