npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eglador-ui-react-sortable

v1.0.0-alpha.1

Published

React Sortable / Repeater component for Eglador UI — drag-and-drop reorder with PayloadCMS-style row UI

Downloads

114

Readme

eglador-ui-react-sortable

npm version npm downloads license dnd-kit bundled tailwind v4 react >= 18 typescript

A drag-and-drop sortable / repeater field for React — PayloadCMS-style row UI on top of @dnd-kit. Compound API, headless useSortableList hook, vertical / horizontal / grid orientations, full keyboard accessibility, Tailwind CSS v4.

Features

  • Compound API<Sortable> + <Sortable.Item> + opt-in row chrome (Radix / shadcn pattern)
  • PayloadCMS-style row UI — drag handle on the left, title in the middle, three-dot action dropdown, collapse chevron on the right
  • Standard row menu set — 7 opt-in menu items (Move up / Move down / Add below / Duplicate / Copy line / Paste line / Remove), each auto-disabled when not applicable
  • 3 orientationsvertical (list), horizontal (chips / tags), grid (any-direction tiles)
  • Sensible defaults — pointer + touch + keyboard sensors, 4 px activation distance, 150 ms touch delay, axis lock, smooth transitions
  • Modifiers built-inrestrictToAxis, restrictToBounds, plus pass any custom @dnd-kit modifier
  • Disabled rows — lock individual items in place
  • Render-prop API — read live index / isDragging / isOver / collapsed per row
  • Headless useSortableList hook — same state model as the component, drive externally with move / add / remove / duplicate / update
  • Accessible — keyboard support via @dnd-kit's KeyboardSensor, ARIA-labeled buttons, focus rings
  • TypeScript-first — generic over your item shape, every prop documented inline
  • @dnd-kit/* bundled — zero extra peer dependencies beyond react / react-dom / tailwindcss

Installation

npm install eglador-ui-react-sortable

Peer dependencies: react >= 18 · react-dom >= 18 · tailwindcss ^4

Setup

Add the following to your global stylesheet so Tailwind picks up the component classes:

@import "tailwindcss";
@source "../node_modules/eglador-ui-react-sortable";

The @source path is relative to the CSS file location:

| Framework | CSS file location | Path | |---|---|---| | Next.js (App Router) | app/globals.css | ../node_modules/eglador-ui-react-sortable | | Next.js (src/) | src/app/globals.css | ../../node_modules/eglador-ui-react-sortable | | Vite | src/index.css | ../node_modules/eglador-ui-react-sortable |

Quick Start

"use client";

import { Sortable, useSortableList } from "eglador-ui-react-sortable";

interface Section { id: string; title: string; body: string; }

export function PageBuilder() {
  const list = useSortableList<Section>({
    initial: [
      { id: "1", title: "Hero",     body: "" },
      { id: "2", title: "Features", body: "" },
      { id: "3", title: "Pricing",  body: "" },
    ],
  });

  return (
    <>
      <Sortable
        items={list.items}
        onChange={list.setItems}
        createItem={() => ({ title: "New section", body: "" })}
      >
        {list.items.map((row) => (
          <Sortable.Item key={row.id} id={row.id} defaultCollapsed>
            <Sortable.Row>
              <Sortable.Header>
                <Sortable.Handle />
                <Sortable.Title>{row.title}</Sortable.Title>
                <Sortable.Menu>
                  <Sortable.MoveUpItem />
                  <Sortable.MoveDownItem />
                  <Sortable.AddBelowItem />
                  <Sortable.MenuSeparator />
                  <Sortable.DuplicateItem />
                  <Sortable.CopyLineItem />
                  <Sortable.PasteLineItem />
                  <Sortable.MenuSeparator />
                  <Sortable.RemoveItem />
                </Sortable.Menu>
                <Sortable.CollapseTrigger />
              </Sortable.Header>
              <Sortable.Content>
                <textarea
                  value={row.body}
                  onChange={(e) => list.update(row.id, { body: e.target.value })}
                />
              </Sortable.Content>
            </Sortable.Row>
          </Sortable.Item>
        ))}
      </Sortable>
      <Sortable.AddButton onClick={() => list.add({ title: "New section", body: "" })} />
    </>
  );
}

API

Exports

| Export | Purpose | |---|---| | Sortable | Compound root — wraps DndContext + SortableContext, configures sensors / orientation / modifiers | | Sortable.Item | One row — owns the useSortable hook, supports disabled + collapse + render-prop | | Sortable.Handle | Drag handle button | | Sortable.Row / .Header / .Title / .Toolbar / .Content | PayloadCMS-style row chrome | | Sortable.CollapseTrigger | Toggles collapsed on the parent Sortable.Item (renders on the far right) | | Sortable.Menu / .MenuItem / .MenuSeparator | Three-dot action dropdown (outside-click + Escape close) | | Sortable.MoveUpItem / .MoveDownItem / .AddBelowItem / .DuplicateItem / .CopyLineItem / .PasteLineItem / .RemoveItem | Pre-built menu items wired to context actions, auto-disabled when not applicable | | Sortable.DuplicateButton / .DeleteButton / .AddButton | Pre-styled action buttons (alternative to Sortable.Menu for a flat toolbar) | | Sortable.Empty | No-rows placeholder | | useSortableList() | Headless state hook — items, setItems, move, add, remove, duplicate, update, reset, indexOf | | useSortableRootContext() / useSortableItemContext() | Read context inside custom subcomponents |

Sortable props

| Prop | Type | Default | Description | |---|---|---|---| | items | T[] | — | Array of items, each with a stable id | | onChange | (items: T[]) => void | — | Fires after every reorder with the new array | | onMove | (event) => void | — | Fires alongside onChange with { activeId, overId, oldIndex, newIndex } | | onDragStart | (id) => void | — | Fires when a drag begins | | onDragEnd | (id) => void | — | Fires when a drag ends (regardless of reorder) | | orientation | "vertical" \| "horizontal" \| "grid" | "vertical" | Sorting strategy + default axis lock | | disabled | boolean | false | Disable sorting for the entire list | | restrictToAxis | boolean | true | Lock the drag preview to the active axis (vertical / horizontal only) | | restrictToBounds | boolean | false | Keep the drag preview inside the parent container | | collisionDetection | CollisionDetection | closestCenter / closestCorners | Override @dnd-kit collision algorithm | | modifiers | Modifier[] | — | Extra @dnd-kit/modifiers to chain after the built-in ones | | createItem | (atIndex: number) => Omit<T, "id"> | — | Factory used by Sortable.AddBelowItem — required to enable that menu item | | generateId | () => string \| number | crypto.randomUUID fallback | ID generator for Add below / Duplicate / Paste line | | dndContextProps | Partial<DndContextProps> | — | Escape hatch for extra DndContext props | | className | string | — | Class on the wrapping <div> | | contentClassName | string | — | Class on the inner items container (overrides default flex / grid layout) |

Sortable.Item props

| Prop | Type | Description | |---|---|---| | id | string \| number | Unique stable identifier | | disabled | boolean | Lock this row in place | | defaultCollapsed | boolean | Initial collapsed state (uncontrolled) | | collapsed | boolean | Controlled collapsed state | | onCollapsedChange | (collapsed: boolean) => void | Fires whenever the collapse state toggles | | children | ReactNode \| (state) => ReactNode | Static markup, or a render-prop receiving { index, isDragging, isOver, isSorting, collapsed, setCollapsed } |

useSortableList(options) returns

| Field | Description | |---|---| | items | Current array | | setItems | React state setter | | move(from, to) | Reorder by index | | moveItem(id, to) | Reorder by id | | add(item, atIndex?) | Insert (returns the resolved id) | | remove(id) | Delete | | duplicate(id) | Copy below original (returns new id) | | update(id, patch) | Partial patch | | reset(next?) | Replace whole list | | indexOf(id) | Find index by id |

Recipes

Minimal list

const [items, setItems] = useState([
  { id: "a", label: "Apple" },
  { id: "b", label: "Banana" },
  { id: "c", label: "Cherry" },
]);

<Sortable items={items} onChange={setItems}>
  {items.map((item) => (
    <Sortable.Item key={item.id} id={item.id}>
      <div className="flex items-center gap-2 p-2 rounded-md border border-zinc-200 bg-white">
        <Sortable.Handle />
        <span>{item.label}</span>
      </div>
    </Sortable.Item>
  ))}
</Sortable>

Grid

<Sortable items={items} onChange={setItems} orientation="grid">
  {items.map((item) => (
    <Sortable.Item key={item.id} id={item.id}>
      <div className="aspect-square rounded-lg border border-zinc-200 bg-white">
        {item.label}
      </div>
    </Sortable.Item>
  ))}
</Sortable>

Disabled rows

<Sortable.Item id={item.id} disabled={item.locked}>
  {/* handle becomes inactive */}
</Sortable.Item>

External controls

const list = useSortableList({ initial });

<Sortable items={list.items} onChange={list.setItems}>{/* … */}</Sortable>

<button onClick={() => list.add({ title: "..." })}>+ add</button>
<button onClick={() => list.move(0, list.items.length - 1)}>send first to end</button>
<button onClick={() => list.reset()}>clear</button>

Render-prop

<Sortable.Item id={item.id}>
  {({ index, isDragging }) => (
    <div className={isDragging ? "border-blue-400 bg-blue-50" : "border-zinc-200"}>
      <Sortable.Handle />
      <span>#{index + 1}</span>
      <span>{item.label}</span>
    </div>
  )}
</Sortable.Item>

Custom modifier

import { restrictToWindowEdges } from "@dnd-kit/modifiers";

<Sortable items={items} onChange={setItems} modifiers={[restrictToWindowEdges]}>
  {/* … */}
</Sortable>

Compatibility

Works with any React-based framework: Next.js, Remix, Vite + React, Gatsby.

Components are marked "use client" (drag-and-drop requires the DOM). On the server they render a static layout; the drag interaction begins on client mount.

Development

npm install
npm run dev               # tsup watch mode
npm run build             # production build to dist/
npm run typecheck         # tsc --noEmit
npm run storybook         # Storybook dev (http://localhost:6006)
npm run build-storybook   # static Storybook export

Publishing

Publishing is automated via GitHub Actions. When a GitHub Release is created, the package is published to npm.

  1. Update version in package.json
  2. Commit and push
  3. Create a GitHub Release with a matching tag (e.g. v1.0.0)

Author

Kenan Gündoğan — https://github.com/kenangundogan

Maintained under Eglador

License

MIT