@apix-js/sortable-list
v1.0.0
Published
A lightweight, framework-agnostic sortable list web component with drag-and-drop functionality
Maintainers
Readme
@apix-js/sortable-list
A lightweight, framework-agnostic sortable list web component with drag-and-drop functionality and smooth FLIP animations.
Built with native Web Components APIs, this sortable list works seamlessly with vanilla JavaScript or any modern framework (React, Vue, Angular, Svelte, etc.).
Installation
NPM
npm install @apix-js/sortable-listPNPM
pnpm add @apix-js/sortable-listYarn
yarn add @apix-js/sortable-listCDN
<!-- ES Module -->
<script type="module">
import 'https://unpkg.com/@apix-js/sortable-list/dist/sortable-list.es.js';
</script>
<!-- UMD (for older browsers) -->
<script src="https://unpkg.com/@apix-js/sortable-list/dist/sortable-list.umd.js"></script>Usage
Basic Usage (HTML)
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<!-- Using slotted items -->
<sortable-list>
<sortable-item id="1">🎨 Design mockups</sortable-item>
<sortable-item id="2">⚡ Build features</sortable-item>
<sortable-item id="3">✅ Write tests</sortable-item>
</sortable-list>
<script type="module">
import '@apix-js/sortable-list';
const list = document.querySelector('sortable-list');
// Listen to reorder events
list.addEventListener('sortable-change', (e) => {
console.log('Items reordered:', e.detail);
console.log('Old index:', e.detail.oldIndex);
console.log('New index:', e.detail.newIndex);
console.log('New order:', e.detail.items);
});
</script>
</body>
</html>Programmatic API (JavaScript)
import { SortableList } from '@apix-js/sortable-list';
const list = document.querySelector('sortable-list');
// Set items programmatically
list.setItems([
{ id: '1', content: 'Task 1' },
{ id: '2', content: 'Task 2' },
{ id: '3', content: 'Task 3' }
]);
// Get current items
const items = list.getItems();
// Add a single item
list.addItem({ id: '4', content: 'Task 4' });
// Remove item by ID
list.removeItem('2');
// Clear all items
list.clear();TypeScript
import '@apix-js/sortable-list';
import type { SortableList, SortableListEventDetails } from '@apix-js/sortable-list';
const list = document.querySelector<SortableList>('sortable-list')!;
list.addEventListener('sortable-change', (e: Event) => {
const event = e as CustomEvent<SortableListEventDetails>;
const { items, oldIndex, newIndex } = event.detail;
// Full type safety!
items.forEach(item => {
console.log(item.id, item.content);
});
});React
import { useEffect, useRef } from 'react';
import '@apix-js/sortable-list';
import '@apix-js/sortable-list/dist/style.css';
import type { SortableList, SortableListEventDetails } from '@apix-js/sortable-list';
// Declare custom element for TypeScript
declare global {
namespace JSX {
interface IntrinsicElements {
'sortable-list': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
'sortable-item': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
}
}
}
function TodoList() {
const listRef = useRef<SortableList>(null);
useEffect(() => {
const list = listRef.current;
if (!list) return;
const handleChange = (e: Event) => {
const event = e as CustomEvent<SortableListEventDetails>;
console.log('Order changed:', event.detail.items);
};
list.addEventListener('sortable-change', handleChange);
return () => list.removeEventListener('sortable-change', handleChange);
}, []);
return (
<sortable-list ref={listRef}>
<sortable-item id="1">🎨 Design mockups</sortable-item>
<sortable-item id="2">⚡ Build features</sortable-item>
<sortable-item id="3">✅ Write tests</sortable-item>
</sortable-list>
);
}Vue 3
<template>
<sortable-list @sortable-change="handleChange">
<sortable-item id="1">🎨 Design mockups</sortable-item>
<sortable-item id="2">⚡ Build features</sortable-item>
<sortable-item id="3">✅ Write tests</sortable-item>
</sortable-list>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import '@apix-js/sortable-list';
import '@apix-js/sortable-list/dist/style.css';
import type { SortableListEventDetails } from '@apix-js/sortable-list';
const handleChange = (e: CustomEvent<SortableListEventDetails>) => {
console.log('Items reordered:', e.detail.items);
};
</script>Vue Config (vite.config.ts):
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// Treat all tags starting with 'sortable-' as custom elements
isCustomElement: tag => tag.startsWith('sortable-')
}
}
})
]
});Angular
// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit } from '@angular/core';
import '@apix-js/sortable-list';
@Component({
selector: 'app-root',
template: `
<sortable-list (sortable-change)="handleChange($event)">
<sortable-item id="1">🎨 Design mockups</sortable-item>
<sortable-item id="2">⚡ Build features</sortable-item>
<sortable-item id="3">✅ Write tests</sortable-item>
</sortable-list>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppComponent {
handleChange(event: Event) {
const customEvent = event as CustomEvent;
console.log('Items reordered:', customEvent.detail.items);
}
}Svelte
<script>
import { onMount } from 'svelte';
import '@apix-js/sortable-list';
import '@apix-js/sortable-list/dist/style.css';
function handleChange(e) {
console.log('Items reordered:', e.detail.items);
}
</script>
<sortable-list on:sortable-change={handleChange}>
<sortable-item id="1">🎨 Design mockups</sortable-item>
<sortable-item id="2">⚡ Build features</sortable-item>
<sortable-item id="3">✅ Write tests</sortable-item>
</sortable-list>API Reference
Attributes
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| disabled | boolean | false | Disables sorting functionality |
| animation-duration | string \| number | "300" | Animation duration in milliseconds (0-1000) |
Example:
<sortable-list disabled animation-duration="500">
<!-- Items are not draggable, animations take 500ms -->
</sortable-list>Methods
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| setItems(items) | SortableListItem[] | void | Set items programmatically |
| getItems() | - | SortableListItem[] | Get current items (returns a copy) |
| addItem(item) | SortableListItem | void | Add a single item |
| removeItem(id) | string | void | Remove item by ID |
| clear() | - | void | Clear all items |
Example:
const list = document.querySelector('sortable-list');
// Set items
list.setItems([
{ id: '1', content: 'Buy milk' },
{ id: '2', content: 'Walk dog' }
]);
// Get items
const items = list.getItems();
// Add item
list.addItem({ id: '3', content: 'Code review' });
// Remove item
list.removeItem('2');
// Clear all
list.clear();Events
| Event | Detail Type | Description |
|-------|------------|-------------|
| sortable-change | SortableListEventDetails | Fired when items are reordered via drag-and-drop |
Event Detail Structure:
interface SortableListEventDetails {
items: SortableListItem[]; // New order of all items
oldIndex: number; // Original position
newIndex: number; // New position
}Example:
list.addEventListener('sortable-change', (e) => {
const { items, oldIndex, newIndex } = e.detail;
console.log(`Item moved from position ${oldIndex} to ${newIndex}`);
// Sync with backend
saveOrder(items);
});TypeScript Types
interface SortableListItem {
id: string;
content: string;
}
interface SortableListEventDetails {
items: SortableListItem[];
oldIndex: number;
newIndex: number;
}Styling
The component uses Shadow DOM for style encapsulation. You can style the host element:
/* Style the container */
sortable-list {
display: block;
max-width: 600px;
margin: 0 auto;
}
/* Style when disabled */
sortable-list[disabled] {
opacity: 0.6;
pointer-events: none;
}For slotted items, you can apply styles directly:
sortable-item {
/* Your custom styles */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: bold;
}
sortable-item:hover {
transform: scale(1.05);
}Features
✨ Framework Agnostic - Works with vanilla JS, React, Vue, Angular, Svelte, and more
🎨 Smooth Animations - Built-in FLIP animations for seamless reordering
🎯 Dual Mode - Use HTML slots or programmatic API
♿ Accessibility - Respects prefers-reduced-motion for accessibility
📦 Lightweight - Zero dependencies, ~5KB gzipped
🔧 TypeScript - Full type definitions included
🎭 Shadow DOM - Style encapsulation, no CSS conflicts
🚀 Modern Build - ES modules, UMD, and CDN support
Browser Support
- Chrome/Edge 67+
- Firefox 63+
- Safari 13.1+
- All modern browsers supporting:
- Custom Elements V1
- Shadow DOM V1
- ES2015+
Credits
Created by: Gihan Rangana
Organization: Apix JS
Repository: github.com/apix-js/web-components
Inspiration
Built with inspiration from:
- SortableJS - Drag-and-drop library
- Lit - Web Components framework
- FLIP Technique by Paul Lewis
Technologies
- Native Web Components (Custom Elements V1, Shadow DOM V1)
- TypeScript
- Vite (Build tool)
- HTML5 Drag and Drop API
Links:
