svelte-sortable-list Svelte Themes

Svelte Sortable List

Create accessible reorderable lists in Svelte

Svelte Sortable List

A package to create accessible sortable lists in Svelte.

Live demos:

Table of contents

Features

  • Mouse, keyboard and touch support.
  • Screen reader support (customizable).
  • Vertical and horizontal direction.
  • Varying heights.
  • Drag handle.
  • Drop marker.
  • Auto scrolling.
  • Lockable axis.
  • Remove on drop outside.
  • Nested interactive elements support.
  • <dialog> support.
  • RTL support.
  • Un-opinionated styling.
  • Typescript definitions.
  • No dependencies.

Get started

Install it

pnpm install @rodrigodagostino/svelte-sortable-list
npm install @rodrigodagostino/svelte-sortable-list
yarn add @rodrigodagostino/svelte-sortable-list

Import it

<script lang="ts">
    import {
        SortableItem,
        SortableList,
        type SortableListProps,
    } from '@rodrigodagostino/svelte-sortable-list';
</script>

Use it

<script lang="ts">
    import {
        SortableItem,
        SortableList,
        type SortableItemData,
        sortItems,
    } from '@rodrigodagostino/svelte-sortable-list';

    let items: SortableItemData[] = [
        {
            id: 'list-item-1',
            text: 'List item 1',
        },
        {
            id: 'list-item-2',
            text: 'List item 2',
        },
        {
            id: 'list-item-3',
            text: 'List item 3',
        },
        {
            id: 'list-item-4',
            text: 'List item 4',
        },
        {
            id: 'list-item-5',
            text: 'List item 5',
        },
    ];

    function handleDragEnd(event: CustomEvent<DragEndEventDetail>) {
        const { draggedItemIndex, targetItemIndex, isCanceled } = event.detail;
        if (!isCanceled && typeof targetItemIndex === 'number' && draggedItemIndex !== targetItemIndex)
            items = sortItems(items, draggedItemIndex, targetItemIndex);
    }
</script>

<SortableList on:dragend={handleDragEnd}>
    {#each items as item, index (item.id)}
        <SortableItem {...item} {index}>
            <div class="ssl-content">
                <span class="ssl-content__text">{item.text}</span>
            </div>
        </SortableItem>
    {/each}
</SortableList>

Accessibility

The following is a list of the accessibility features provided by the package:

Keyboard navigation

These are the steps to navigate and operate the list:

  1. Press Tab to focus the list.
  2. Press Arrow Up, Arrow Left, Arrow Down, Arrow Right, Home or End to focus the first item in the list.
  3. Press Arrow Up or Arrow Left to move the focus to the previous item.
  4. Press Arrow Down or Arrow Right to move the focus to the next item.
  5. Press Home to move the focus to the first item.
  6. Press End to move the focus to the last item.
  7. Press Space to start dragging an item. When dragging, press Space again to drop the dragged item.
  8. Press Arrow Up or Arrow Left to move the dragged item to the previous position.
  9. Press Arrow Down or Arrow Right to move the dragged item to the next position.
  10. Press Home to move the dragged item to the first position.
  11. Press End to move the dragged item to the last position.
  12. Press Escape to cancel the drag and return the item to its initial position.

Screen reader announcements customization

There are two main things that need to be considered to customize the screen reader announcements:

  • The aria-description attribute: the keyboard navigation instructions (default: Press the arrow keys to move through the list items. Press Space to start dragging an item. When dragging, use the arrow keys to move the item around. Press Space again to drop the item, or Escape to cancel.).
  • The announcements prop: the announcements to be read out by the screen reader during drag and drop operations.

In addition to those, you can also use the following too (accepted by both the <SortableList> and <SortableItem> components):

  • The aria-label attribute: the name of the list.
  • The aria-labelledby attribute: the ID of the element that provides the name of the list.

The following example contains most of the code that you can find in the “With custom announcements” demo page, and it shows how we could translate the announcements to Spanish:

<script lang="ts">
    ...

    const announcements: SortableListProps['announcements'] = {
        lifted: (_, draggedItemIndex) => {
            return `Ha levantado un item en la posición ${draggedItemIndex! + 1}.`;
        },
        dragged: (_, draggedItemIndex, __, targetItemIndex) => {
            const startPosition = draggedItemIndex + 1;
            const endPosition = targetItemIndex + 1;
            const result =
                startPosition !== endPosition
                    ? `desde la posición ${startPosition} a la posición ${endPosition}`
                    : `de vuelta a su posición inicial de ${startPosition}`;
            return `Ha movido el item ${result}.`;
        },
        dropped: (_, draggedItemIndex, __, targetItemIndex) => {
            const startPosition = draggedItemIndex + 1;
            const endPosition = typeof targetItemIndex === 'number' ? targetItemIndex + 1 : null;
            const result =
                endPosition === null
                    ? `Se ha mantenido en su posición inicial de ${startPosition}`
                    : startPosition !== endPosition
                        ? `Se ha movido desde la posición ${startPosition} a la posición ${endPosition}`
                        : `Ha vuelto a su posición inicial de ${startPosition}`;
            return `Ha soltado el item. ${result}.`;
        },
        canceled: (_, draggedItemIndex) => {
            return `Ha cancelado el arrastre. El item ha vuelto a su posición inicial de ${draggedItemIndex + 1}.`;
        },
    };
</script>

<SortableList
    ...
    aria-description="Presione las flechas para desplazarte por los elementos de la lista. Presione Espacio para empezar a arrastrar un elemento. Al arrastrar, use las flechas para moverlo. Presione Espacio de nuevo para soltar el elemento o Escape para cancelar."
    {announcements}
>
    ...
</SortableList>

Components

The following is a list of the available components inside the package:

Component Description
<SortableList> The primary container. Provides the main structure, the drag-and-drop interactions and emits the available events.
<SortableItem> An individual item within <SortableList>. Holds the data and content for each list item, as well as the <Handle> and <Remove> components when needed.
<Handle> An element that limits the draggable area of a list item to itself. Including it inside a <SortableItem> will directly activate the handle functionality for that item.
<Remove> A <button> element that (when pressed) removes an item. Including it inside a <SortableItem> will directly allow it to dispatch the remove event for that item.

It would be possible to avoid the <Remove> component and just go with a <button> element to trigger the removal of an item, but keep in mind that this component doesn’t just fire a click event, it will also focus the next item in the list if the user is using the keyboard to press the remove button. Otherwise, after the focused element is removed, the focus will fall back to the <body>.

<SortableList> props

Prop Type Default Possible values Description
gap number | undefined 12 Number equal to or above 0. Separation between items (in pixels).
direction string | undefined 'vertical' 'vertical' or 'horizontal' Orientation in which items will be arranged.
transitionDuration number | undefined 240 Number equal to or above 0. Time the transitions for the ghost (dropping) and items (translation, addition, removal) take to complete (in milliseconds). Assign it a value of 0 to remove animations.
hasDropMarker boolean | undefined false true or false If true, displays a position marker representing where the dragged item will be positioned when drag-and-dropping.
hasLockedAxis boolean | undefined false true or false If true, prevents the dragged item from moving away from the main axis.
hasBoundaries boolean | undefined false true or false If true, items will only be draggable inside the list limits.
canClearOnDragOut boolean | undefined false true or false If true, the target item will be cleared when a the dragged item (by a pointing device) does not collide with any of the items in the list. This will cause the dragged item to return to its initial position when dropped. Otherwise, it will take the position of the last item it collided with.
canRemoveOnDropOut boolean | undefined false true or false If true, items will be removed when dragged and dropped outside of the list boundaries. This needs to be coupled with the on:remove event handler for it to complete the removal process.
isLocked boolean | undefined false true or false If true, will allow every item in the list to be focused, but will prevent them from being dragged (both through pointer and keyboard). Interactive elements inside will operate normally.
isDisabled boolean | undefined false true or false If true, will allow every item in the list to be focused, but will prevent them from being dragged (both through pointer and keyboard) and change its appearance to dimmed. Interactive elements inside will be disabled.

<SortableList> events

[!Note] Events are fired in the order they are displayed below.

Event Type Trigger Returns
on:mounted CustomEvent<MountedEventDetail> The component is mounted.
event: {
  detail: null
}
on:dragstart CustomEvent<DragStartEventDetail> An item starts to be dragged by a pointer device or a keyboard.
event: {
  detail: {
    deviceType: 'pointer' | 'keyboard',
    draggedItem: HTMLLIElement,
    draggedItemId: string,
    draggedItemIndex: number,
    isBetweenBounds: boolean,
    canRemoveOnDropOut: boolean
  }
}
on:drag CustomEvent<DragEventDetail> A dragged item is moved around by a pointer device or a keyboard (fires every few hundred milliseconds).
event: {
  detail: {
    deviceType: 'pointer' | 'keyboard',
    draggedItem: HTMLLIElement,
    draggedItemId: string,
    draggedItemIndex: number,
    targetItem: HTMLLIElement | null,
    targetItemId: string | null,
    targetItemIndex: number | null,
    isBetweenBounds: boolean,
    canRemoveOnDropOut: boolean
  }
}
on:drop CustomEvent<DropEventDetail> A dragged item is released by a pointer device or a keyboard.
event: {
  detail: {
    deviceType: 'pointer' | 'keyboard',
    draggedItem: HTMLLIElement,
    draggedItemId: string,
    draggedItemIndex: number,
    targetItem: HTMLLIElement | null,
    targetItemId: string | null,
    targetItemIndex: number | null,
    isBetweenBounds: boolean,
    canRemoveOnDropOut: boolean
  }
}
on:dragend CustomEvent<DragEndEventDetail> A dragged item reaches its destination after being released.
event: {
  detail: {
    deviceType: 'pointer' | 'keyboard',
    draggedItem: HTMLLIElement,
    draggedItemId: string,
    draggedItemIndex: number,
    targetItem: HTMLLIElement | null,
    targetItemId: string | null,
    targetItemIndex: number | null,
    isBetweenBounds: boolean,
    canRemoveOnDropOut: boolean,
    isCanceled: boolean
  }
}

<SortableItem> props

Prop Type Default Possible values Description
id string undefined Unique string. Unique identifier for each item.
index number undefined Unique number. Position of the item in the list.
isLocked boolean | undefined false true or false If true, will prevent the item from being dragged.
isDisabled boolean | undefined false true or false If true, will prevent the item from being dragged and change its appearance to dimmed.

Utilities

Function Description
sortItems() Provides an easy mechanism to reorder items (it’s recommended to be used in combination with the on:dragend event).
removeItem() Provides an easy mechanism to remove an item from your list (should be used in combination with the on:drop event).

Example:

<script lang="ts">
    import { ... removeItem, sortItems } from '$lib/index.js';
    ...

    function handleDragEnd(event: CustomEvent<DragEndEventDetail>) {
        const { draggedItemIndex, targetItemIndex, isCanceled } = event.detail;
        if (!isCanceled && typeof targetItemIndex === 'number' && draggedItemIndex !== targetItemIndex)
            items = sortItems(items, draggedItemIndex, targetItemIndex);
    }

    function handleRemoveClick(event: MouseEvent) {
        const target = event.target as HTMLElement;
        const item = target.closest<HTMLLIElement>('.ssl-item');
        const itemIndex = Number(item?.dataset.itemIndex);
        if (!item || itemIndex < 0) return;
        items = removeItem(items, itemIndex);
    }
</script>

<SortableList on:dragend={handleDragEnd}>...</SortableList>

Types

Type Description
SortableListProps Provides definitions for the <SortableList> component.
SortableItemProps Provides definitions for the <SortableItem> component.
SortableItemData Provides definitions for your items list data.
MountedEventDetail Provides definitions for the <SortableList> mounted custom event detail.
DragStartEventDetail Provides definitions for the <SortableList> dragstart custom event detail.
DragEventDetail Provides definitions for the <SortableList> drag custom event detail.
DropEventDetail Provides definitions for the <SortableList> drop custom event detail.
DragEndEventDetail Provides definitions for the <SortableList> dragend custom event detail.

Example:

<script lang="ts">
    import type { ... DragEndEventDetail, DropEventDetail } from '$lib/types/index.js';
    ...

    function handleDrop(event: CustomEvent<DropEventDetail>) {
        const { draggedItemIndex, isBetweenBounds, canRemoveOnDropOut } = event.detail;
        if (!isBetweenBounds && canRemoveOnDropOut) items = removeItem(items, draggedItemIndex);
    }

    function handleDragEnd(event: CustomEvent<DragEndEventDetail>) {
        const { draggedItemIndex, targetItemIndex, isCanceled } = event.detail;
        if (!isCanceled && typeof targetItemIndex === 'number' && draggedItemIndex !== targetItemIndex)
            items = sortItems(items, draggedItemIndex, targetItemIndex);
    }
</script>

Styles

If you want to make use of the styles present in the demo pages, import them in your project like so:

<script>
    import 'svelte-sortable-list/styles.css';
</script>

Selectors

[!IMPORTANT] To customize the appearance of the list items and ghost element (and not cause any conflicts or interferences with the core styles and transitions), the usage of the .ssl-item and .ssl-ghost selectors must be avoided. You can instead create a wrapping element for the item’s content (like the element with the .ssl-content class included in one of the examples), which should be the direct child of the element with the .ssl-item class (the ghost element will simply mirror the list item’s content and appearance).

This is a list of the selectors you can use to style the list and the list items to your heart’s desire:

Selector Points to
.ssl-list The <SortableList> main container.
.ssl-list[aria-orientation="vertical"] The <SortableList> main container when direction is set to vertical.
.ssl-list[aria-orientation="horizontal"] The <SortableList> main container when direction is set to horizontal.
.ssl-list[data-has-drop-marker="true"] The <SortableList> main container while hasDropMarker is enabled.
.ssl-list[data-can-remove-on-drop-out="true"] The <SortableList> main container while canRemoveOnDropOut is enabled.
.ssl-list[data-is-locked="true"] The <SortableList> that is locked.
.ssl-list[aria-disabled="true"] The <SortableList> that is disabled.
.ssl-item Each <SortableItem> main container.
.ssl-item[data-is-pointer-dragging="true"] The <SortableItem> that is being dragged by a pointing device.
.ssl-item[data-is-pointer-dropping="true"] The <SortableItem> that is being dropped by a pointing device.
.ssl-item[data-is-keyboard-dragging="true"] The <SortableItem> that is being dragged by the keyboard.
.ssl-item[data-is-keyboard-dropping="true"] The <SortableItem> that is being dropped by the keyboard.
.ssl-item[data-is-locked="true"] Each <SortableItem> that is locked.
.ssl-item[aria-disabled="true"] Each <SortableItem> that is disabled.
.ssl-item[data-is-removing="true"] The <SortableItem> that is being removed by dropping it outside the list limits by a pointing device.
.ssl-ghost The shadow element displayed under the pointer when dragging.
.ssl-ghost[data-is-pointer-dragging="true"] The shadow element while it’s being dragged by a pointing device.
.ssl-ghost[data-is-pointer-dropping="true"] The shadow element while it’s being dropped by a pointing device.
.ssl-ghost[data-is-between-bounds="true"] The shadow element while it’s inside the list limits.
.ssl-ghost[data-is-removing="true"] The shadow element while a <SortableItem> is being removed.
.ssl-ghost[data-can-remove-on-drop-out="true"] The shadow element while canRemoveOnDropOut is enabled.
.ssl-handle The <Handle> main container.
.ssl-remove The <Remove> main container.

[!TIP] Combining the available selectors appropriately should be enough to style the list and the list items to your heart’s content. For example, the following would target the direct child of the <Ghost> component when canRemoveOnDropOut is enabled and is being dragged outside of the list limits:

.ssl-ghost[data-can-remove-on-drop-out="true"][data-is-between-bounds="false"] .ssl-content {
  ...
}

If you find that your particular case is not covered, please, feel free to submit a request :)

Custom properties

Custom property Description
--gap Separation between items (in pixels).
--transition-duration Time the transitions for the ghost (dropping) and items (translation, addition, removal) take to complete (in milliseconds).

Motivation

While working on a SvelteKit project, I ran into the need of adding drag-and-drop capabilities to a couple of items lists, for which I decided to make use of SortableJS, which is certainly a popular option. I implemented it through a Svelte Action and it provided just what I needed, or so it seemed. After a while I realized I was not only missing touch screen support (since it was built with the HTML Drag and Drop API), but also accessibility was nowhere to be seen, and seems there are no plans to work on it.

I was not able to find any other suitable option, so this problem felt like a good opportunity to build my own package. And so while doing some research to try and understand the implications of such feature, I ran into a very interesting article and a very interesting talk by Vojtech Miksu which really guided me through the different paths available, their advantages, pain points and limitations to create a drag-and-drop system, putting particular focus on accessibility and touch screen support.

Even though React Movable was built for React, it served as my main inspiration when building this package. So thank you again, Vojtech :)

Top categories

Loading Svelte Themes