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

@nizalix/arrange-kanban

v0.1.0

Published

Kanban board drag and drop for React (Arrange): move cards and columns, WIP limits, transition rules, keyboard support and optimistic updates.

Readme

@nizalix/arrange-kanban

Drag-and-drop Kanban board for React and Next.js. Move cards between columns, reorder columns, enforce workflow rules and WIP limits, save with optimistic updates.

npm types license

Build a Trello- or Jira-style board without writing drag-and-drop logic. You describe your columns and cards as plain data and draw them however you like (Tailwind, Bootstrap, CSS Modules...). Arrange handles dragging, keyboard moves, rules and accessibility.

  • Drag cards inside a column and across columns, drag columns to reorder
  • Rules: locked cards or columns, WIP limits, allowed workflow transitions, custom checks
  • Optimistic updates with automatic rollback if your server rejects a move
  • Keyboard support: Space pick up / drop, arrows to move, Alt + Arrow keys as a shortcut, Esc to cancel
  • Screen-reader announcements
  • A pure, framework-free part (moveCard, moveColumn...) that works on the server and in tests

Install

npm install @nizalix/arrange-kanban @nizalix/arrange-react

Requires React 18 or 19. Import the React parts from @nizalix/arrange-kanban/react and the pure functions from @nizalix/arrange-kanban.

Quick start

"use client";

import { useState } from "react";
import { KanbanBoard } from "@nizalix/arrange-kanban/react";

type Card = { id: string; title: string };
type Column = { id: string; title: string; cards: Card[] };

const initial: Column[] = [
  { id: "todo", title: "To do", cards: [{ id: "c1", title: "Design the logo" }, { id: "c2", title: "Write the docs" }] },
  { id: "doing", title: "In progress", cards: [{ id: "c3", title: "Build the board" }] },
  { id: "done", title: "Done", cards: [] },
];

export function Board() {
  const [columns, setColumns] = useState(initial);

  return (
    <KanbanBoard
      columns={columns}
      onChange={(next) => setColumns([...next])}
      renderBoard={(cols) => <div style={{ display: "flex", gap: 16 }}>{cols}</div>}
      renderColumn={(state, cards) => (
        <section
          ref={state.setNodeRef}
          style={{ ...state.style, width: 260, padding: 12, background: state.isDropTarget ? "#eef2ff" : "#f4f4f5" }}
        >
          <h3>{state.column.title} ({state.cardCount})</h3>
          <div style={{ display: "grid", gap: 8, minHeight: 40 }}>{cards}</div>
        </section>
      )}
      renderCard={(card, state) => (
        <article
          ref={state.setNodeRef}
          style={{ ...state.style, padding: 10, background: "white", opacity: state.isDragging ? 0.4 : 1 }}
          {...state.attributes}
        >
          {card.title}
        </article>
      )}
      renderOverlay={(info) =>
        info.card ? <article style={{ padding: 10, background: "white", boxShadow: "0 8px 24px #0003" }}>{info.card.title}</article> : null
      }
    />
  );
}

How it works:

  • Your board is an array of columns, each with a cards array. Add any extra fields you want (title, assignee...).
  • onChange gives you the next board. Store it in state (and save it to your API).
  • renderColumn receives the column state and the already-rendered cards to place inside.
  • renderCard receives the card and its state: attach setNodeRef, style and spread attributes.
  • renderOverlay draws the floating card while dragging. Omit it to move the real element instead.

Rules: locking, WIP limits, workflows

<KanbanBoard
  columns={columns}
  onChange={(next) => setColumns([...next])}
  rules={{
    // Locked cards cannot be moved at all.
    isCardLocked: (card) => card.id === "c3",
    // Locked columns cannot be reordered (their cards can still move).
    isColumnLocked: (column) => column.id === "done",
    // WIP limit: only enforced when a card arrives from another column.
    maxCards: (column) => (column.id === "doing" ? 3 : undefined),
    // Allowed workflow: todo -> doing -> done. Moving inside a column is always allowed.
    transitions: { todo: ["doing"], doing: ["todo", "done"] },
    // Final custom veto.
    canMoveCard: ({ card, to }) =>
      to.id === "done" && card.title.length === 0 ? { ok: false, reason: "Give the card a title first." } : true,
  }}
  onMoveRejected={(failure) => toast(failure.message)}
  …
/>

onMoveRejected receives a human-readable message (safe to show in your UI) and a reason code: locked, not-accepted, limit-reached, transition-not-allowed, rejected, ...

You can show why a drop is refused while hovering: state.isInvalidOver and state.rejectionReason are available on the column state. Do not rely on color alone; show text or an icon.

Save to your server (optimistic updates with rollback)

useOptimisticBoard updates the UI immediately, saves moves one at a time in order, and rolls back to the last confirmed board if a save fails.

"use client";

import { KanbanBoard, useOptimisticBoard } from "@nizalix/arrange-kanban/react";

export function Board({ initial }: { initial: Column[] }) {
  const board = useOptimisticBoard(initial, {
    persist: async ({ change }) => {
      const res = await fetch("/api/board/move", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(change),
      });
      if (!res.ok) throw new Error("Move rejected"); // throwing rolls the board back
    },
    onRejected: ({ error }) => console.warn("Rolled back:", error),
  });

  return (
    <KanbanBoard
      columns={board.columns}
      onChange={board.onChange}
      renderColumn={…}
      renderCard={…}
    />
  );
}
  • persist may return the server's board (Column[]) to replace the optimistic guess, so the UI always matches the server.
  • board.pending is how many saves are in flight; board.rejection is the most recent failure.
  • change describes what moved: { type: "card", cardId, from: { columnId, index }, to: { columnId, index } } or { type: "column", columnId, from, to }.

For a ready-made, validated server endpoint (rules and permission checks re-run on the server), use @nizalix/arrange-nestjs-adapter. Never trust the browser alone for rules that matter.

Keyboard and accessibility

| Key | Action | | --- | --- | | Tab | Focus a card | | Space | Pick up / drop | | Arrow keys | Move while picked up | | Esc | Cancel | | Alt + ↑ / ↓ | Move the focused card up / down in its column | | Alt + ← / → | Move the focused card to the previous / next column |

Moves are announced to screen readers. Please still test with your own assistive technology before making conformance claims.

Pure functions (no React)

Useful on the server, in tests, or with other frameworks:

import { moveCard, moveColumn, canMoveCard, validateBoard } from "@nizalix/arrange-kanban";

const result = moveCard(columns, { cardId: "c1", toColumnId: "doing", toIndex: 0 }, rules);
if (result.ok) {
  save(result.columns); // new board, input is never mutated
  console.log(result.change); // what changed
} else {
  console.log(result.reason, result.message); // e.g. "limit-reached"
}

toIndex is the position in the destination column after the card has left its old place.

Props reference (KanbanBoard)

| Prop | Description | | --- | --- | | columns | Required. { id, cards: [{ id }] }[] plus any fields you like | | onChange(columns, change) | Called with the next board after an accepted move | | renderColumn(state, cards) | Required. Draw a column; attach state.setNodeRef | | renderCard(card, state, column) | Required. Draw a card; attach state.setNodeRef, state.style, spread state.attributes | | renderOverlay(info) | Floating preview (info.type, info.card, info.column) | | renderBoard(columns) | Wrap all columns (e.g. in a flex container) | | rules | isCardLocked, isColumnLocked, acceptsCard, maxCards, transitions, canMoveCard, canMoveColumn | | onMoveRejected(failure, attempted) | A move was refused by the rules | | getCardLabel, getColumnLabel | Names used in screen-reader announcements (defaults: title, name or label) | | …provider props | sensors, modifiers, autoScroll, animation, … |

Troubleshooting

  • Nothing happens in Next.js: add "use client" at the top of the file.
  • Cards snap back: update your state in onChange.
  • Empty columns don't accept drops: attach state.setNodeRef to the column element and give it a min-height.
  • Card ids must be unique across the whole board, not just within a column.
  • Rolled back unexpectedly: your persist threw or rejected. Check the network response.

Related packages

@nizalix/arrange-presets · @nizalix/arrange-tree · @nizalix/arrange-react · @nizalix/arrange-nestjs-adapter

Author

Created and maintained by salmannizam. Issues and ideas are welcome on GitHub. MIT licensed.