export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
export function move(list, from, to) {
const next = list.slice();
const bounded = clamp(to, 0, next.length - 1);
const [item] = next.splice(from, 1);
next.splice(bounded, 0, item);
return next;
}
import { useCallback, useRef, useState } from 'react';
import { move } from './reorder';
export function useSortable(items, onReorder) {
const [dragIndex, setDragIndex] = useState(null);
const rows = useRef(new Map());
const origin = useRef(null);
const targetFor = useCallback((clientY) => {
let target = 0;
for (const [index, node] of rows.current) {
const rect = node.getBoundingClientRect();
if (clientY > rect.top + rect.height / 2) target = index;
}
return target;
}, []);
const getItemProps = useCallback((index) => ({
ref: (node) => {
if (node) rows.current.set(index, node);
else rows.current.delete(index);
},
onPointerDown: (e) => {
e.currentTarget.setPointerCapture(e.pointerId);
origin.current = index;
setDragIndex(index);
},
onPointerMove: (e) => {
if (origin.current === null) return;
const to = targetFor(e.clientY);
if (to === origin.current) return;
onReorder(move(items, origin.current, to));
origin.current = to;
setDragIndex(to);
},
onPointerUp: (e) => {
e.currentTarget.releasePointerCapture(e.pointerId);
origin.current = null;
setDragIndex(null);
}
}), [index, items, onReorder, targetFor]);
return { dragIndex, getItemProps };
}
import React from 'react';
import { useSortable } from './useSortable';
export default function SortableList({ items, setItems }) {
const { dragIndex, getItemProps } = useSortable(items, setItems);
return (
<ul className="sortable" role="list">
{items.map((item, index) => (
<li
key={item.id}
{...getItemProps(index)}
className="sortable__item"
style={{
touchAction: 'none',
opacity: dragIndex === index ? 0.5 : 1,
cursor: 'grab'
}}
>
<span className="sortable__handle" aria-hidden="true">⠿</span>
{item.label}
</li>
))}
</ul>
);
}
This snippet implements a sortable list using the Pointer Events API instead of the legacy HTML5 drag-and-drop API, which gives finer control over touch, mouse, and pen input while sidestepping the browser's inconsistent drag-image rendering. The logic is split into a pure reorder helper, a reusable pointer-drag hook, and the list component that wires them together.
In reorder.js, the move function is a small immutable array operation: it clones the input with slice(), splices the item out of from, and inserts it back at to. Keeping this pure means the reorder can be unit-tested in isolation and never mutates React state directly, which is essential for predictable re-renders. clamp guards against out-of-range indices that can arise while the pointer travels past the list edges.
The useSortable hook owns all the imperative pointer bookkeeping. On pointerdown it records the grabbed index and calls setPointerCapture so the element keeps receiving events even if the pointer leaves it — a critical detail that makes dragging reliable on touchscreens. During pointermove it measures each row's midpoint against the pointer's clientY to compute the target index, then calls move and notifies the parent through onReorder. The hook returns dragIndex so the UI can style the item being dragged, and a getItemProps factory that attaches handlers and a ref per row. Registering rows in a Map by index lets the hook read live getBoundingClientRect() values without extra React state.
In SortableList component, the list is controlled: items come from props and every reorder flows back up via setItems, keeping a single source of truth. Each row spreads getItemProps(index), sets touchAction: 'none' to prevent the browser from scrolling mid-drag, and dims the active row using dragIndex.
The trade-off of this approach is that it reimplements hit-testing manually rather than leaning on a library, but it stays dependency-free, works across input types, and remains fully controllable. A production version would add keyboard reordering and aria live-region announcements for accessibility, since pointer-only dragging excludes keyboard users.
Related snips
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Semantic HTML Example</title>
Semantic HTML5 elements and accessibility best practices
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import SwiftUI
struct ContentView: View {
@State private var username = ""
@State private var isLoggedIn = false
@StateObject private var viewModel = LoginViewModel()
SwiftUI declarative UI with state management
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.