react ~5 KB 0 deps v0.1.0 ↗ GitHub ↗

@mshafiqyajid/react-sortable

Headless hook and styled component for drag-and-drop lists. Native pointer events — no external DnD library. Keyboard reorder with live region announcements, spring-animated displaced items, ghost-follows-pointer drag, horizontal and vertical orientation, handle or full-item drag, reduced-motion safe.

Playground #

DesignDesign system tokensDefine colour, spacing, and type scales
DevOpsSet up CI pipelineGitHub Actions for build, test, and publish
DocsWrite onboarding docsQuick-start guide for new contributors
DevComponent libraryHeadless primitives with styled variants
QAPerformance auditLighthouse and bundle-size benchmarks
A11yAccessibility reviewWCAG 2.2 AA compliance across all components
Props
TSX
import { SortableStyled } from "@mshafiqyajid/react-sortable/styled";
import "@mshafiqyajid/react-sortable/styles.css";

<SortableStyled
  items={items}
  onReorder={setItems}
  renderItem={renderItem}
/>

Install #

npm install @mshafiqyajid/react-sortable

Quick start #

import { useState } from "react";
import { SortableStyled } from "@mshafiqyajid/react-sortable/styled";
import "@mshafiqyajid/react-sortable/styles.css";

interface Task {
  id: string;
  title: string;
}

function App() {
  const [items, setItems] = useState<Task[]>([
    { id: "1", title: "Design tokens" },
    { id: "2", title: "Write tests"   },
    { id: "3", title: "Ship it"       },
  ]);

  return (
    <SortableStyled
      items={items}
      onReorder={setItems}
      renderItem={(item, { isDragging }) => (
        <span style={{ opacity: isDragging ? 0.5 : 1 }}>
          {(item as Task).title}
        </span>
      )}
    />
  );
}

Headless hook #

import { useSortable } from "@mshafiqyajid/react-sortable";

const { containerProps, getItemProps, getItemState, activeId, isDragging } =
  useSortable({ items, onReorder: setItems });

const { ref, ...restContainerProps } = containerProps;

return (
  <ul ref={ref} {...restContainerProps}>
    {items.map((item) => {
      const { isDragging, isOver, handleProps } = getItemState(item);
      return (
        <li
          key={item.id}
          {...getItemProps(item)}
          style={{ opacity: isDragging ? 0.4 : 1 }}
        >
          <span {...handleProps} style={{ cursor: "grab" }}>⠿</span>
          {item.label}
        </li>
      );
    })}
  </ul>
);

Keyboard controls #

KeyAction
SpacePick up item at focused position
EnterDrop item at current position
ArrowUp / ArrowDownMove item (vertical list)
ArrowLeft / ArrowRightMove item (horizontal list)
EscapeCancel — restore original order

A live region announces position changes to screen readers during keyboard reordering.

API — SortableStyled #

PropTypeDefaultDescription
itemsSortableItem[]Array of items. Each must have a unique id (string or number).
onReorder(items: SortableItem[]) => voidCalled with the reordered array after every drop or keyboard move.
renderItem(item, state) => ReactNodeRender each row. Receives isDragging, isOver, handleProps.
orientation"vertical" | "horizontal""vertical"List direction. Horizontal uses ArrowLeft/Right for keyboard navigation.
handlebooleantrueShow drag handle grip icon. When false the whole item is the drag target.
disabledbooleanfalseDisable all drag and keyboard interaction.
animationDurationnumber200CSS transition duration in milliseconds for displaced items.
classNamestringExtra class on the container element.
styleCSSPropertiesInline styles on the container element.

renderItem state #

FieldTypeDescription
isDraggingbooleanTrue when this item is the active drag or keyboard-picked item.
isOverbooleanTrue when the pointer is over this item (drop target indicator).
handlePropsobjectSpread onto a custom handle element. Provides pointer and aria attributes.

useSortable hook #

import { useSortable } from "@mshafiqyajid/react-sortable";

const {
  items,          // current items array (same reference passed in)
  containerProps, // spread on the container: role, aria-orientation, data-dragging, ref
  getItemProps,   // (item) => props to spread on each item element
  getItemState,   // (item) => { isDragging, isOver, handleProps }
  activeId,       // id of the dragged/picked item, or null
  overId,         // id of the item currently under the pointer, or null
  isDragging,     // boolean — true when a drag is in progress
  liveRegionText, // current announcement for screen readers
} = useSortable({ items, onReorder, orientation, disabled, animationDuration });

CSS variables #

/* Paste into your stylesheet to customise */
:root {
  --rsort-bg: #f4f4f5;
  --rsort-item-bg: #ffffff;
  --rsort-item-fg: #18181b;
  --rsort-item-border: #e4e4e7;
  --rsort-item-radius: 8px;
  --rsort-item-padding: 10px 14px;
  --rsort-handle-color: #a1a1aa;
  --rsort-gap: 6px;
  --rsort-font-size: 0.875rem;
  --rsort-over-bg: #f0f0ff;
  --rsort-over-border: #6366f1;
  --rsort-active-opacity: 0.4;
  --rsort-duration: 200ms;
  --rsort-focus-ring: 0 0 0 2px #6366f1;
}

ARIA #

Container: role="listbox", aria-orientation, data-dragging when active.

Items: role="option", aria-roledescription="sortable item", aria-grabbed.

Data attributes: data-active on the dragged item; data-over on the drop target; data-orientation on the container.

A visually-hidden live region (role="status", aria-live="assertive") announces position changes during keyboard reorder and cancellation.

Edit this page on GitHub