Drag-and-Drop Interface (Kanban Board) Implementation for Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing Drag-and-Drop Interface (Kanban Board) on Website

Drag-and-drop at first glance seems simple — until you start dealing with touch devices, autoscroll when dragging to edge, nested containers, and accessibility. HTML5 Drag-and-Drop API covers basic scenarios, but for Kanban board with multiple columns and nested lists, you need a library.

Library Choice

@dnd-kit/core — modern standard for React. Works with touch and keyboard, accessibility support out of box, doesn't depend on DOM order. Size ~10 KB gzipped.

react-beautiful-dnd — popular, but development frozen. Don't use in new projects.

Sortable.js — vanilla option, works with any framework, but requires more manual work for React state integration.

Installing dnd-kit

npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Data Structure

type CardId = string
type ColumnId = string

interface Card {
  id: CardId
  title: string
  description?: string
  assignee?: string
  priority: 'low' | 'medium' | 'high'
}

interface Column {
  id: ColumnId
  title: string
  cardIds: CardId[]
}

interface BoardState {
  cards: Record<CardId, Card>
  columns: Record<ColumnId, Column>
  columnOrder: ColumnId[]
}

Normalized structure (cards separate from columns) simplifies moving: just update cardIds in the needed columns.

DndContext and Sensors

import {
  DndContext,
  DragEndEvent,
  DragOverEvent,
  DragStartEvent,
  PointerSensor,
  KeyboardSensor,
  TouchSensor,
  useSensor,
  useSensors,
  closestCorners,
} from '@dnd-kit/core'
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'

function KanbanBoard() {
  const [board, setBoard] = useState<BoardState>(initialBoard)
  const [activeCardId, setActiveCardId] = useState<CardId | null>(null)

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8, // pixels before DnD starts — so clicks work
      },
    }),
    useSensor(TouchSensor, {
      activationConstraint: {
        delay: 250,       // delay on touch — avoid scroll conflict
        tolerance: 5,
      },
    }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  )

  function handleDragStart(event: DragStartEvent) {
    setActiveCardId(event.active.id as CardId)
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event
    if (!over) return

    const activeColId = findColumnByCardId(board, active.id as CardId)
    const overColId = isColumnId(over.id)
      ? (over.id as ColumnId)
      : findColumnByCardId(board, over.id as CardId)

    if (!activeColId || !overColId || activeColId === overColId) return

    // Move card between columns during drag (live preview)
    setBoard((prev) => moveCardBetweenColumns(prev, active.id as CardId, activeColId, overColId, over.id as CardId))
  }

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event
    setActiveCardId(null)

    if (!over) return

    const activeColId = findColumnByCardId(board, active.id as CardId)
    const overColId = isColumnId(over.id)
      ? (over.id as ColumnId)
      : findColumnByCardId(board, over.id as CardId)

    if (!activeColId || !overColId) return

    if (activeColId === overColId) {
      // Sort within one column
      setBoard((prev) => reorderCardInColumn(prev, activeColId, active.id as CardId, over.id as CardId))
    }
    // Between columns already handled in handleDragOver
  }

  const activeCard = activeCardId ? board.cards[activeCardId] : null

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCorners}
      onDragStart={handleDragStart}
      onDragOver={handleDragOver}
      onDragEnd={handleDragEnd}
    >
      <div className="flex gap-4 overflow-x-auto p-4">
        {board.columnOrder.map((colId) => (
          <KanbanColumn
            key={colId}
            column={board.columns[colId]}
            cards={board.columns[colId].cardIds.map((id) => board.cards[id])}
          />
        ))}
      </div>
      <DragOverlay>
        {activeCard ? <KanbanCard card={activeCard} isDragging /> : null}
      </DragOverlay>
    </DndContext>
  )
}

DragOverlay renders a "ghost" card on top of everything — without it, the card disappears from column during dragging, which looks bad.

SortableContext and Column

import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { useDroppable } from '@dnd-kit/core'
import { CSS } from '@dnd-kit/utilities'

function KanbanColumn({ column, cards }: { column: Column; cards: Card[] }) {
  // Column as drop-target for empty zones
  const { setNodeRef, isOver } = useDroppable({ id: column.id })

  return (
    <div
      className={`w-72 rounded-lg bg-gray-100 p-3 flex-shrink-0 ${
        isOver ? 'ring-2 ring-blue-400' : ''
      }`}
    >
      <h3 className="font-semibold mb-3">{column.title}</h3>
      <SortableContext
        items={cards.map((c) => c.id)}
        strategy={verticalListSortingStrategy}
      >
        <div ref={setNodeRef} className="space-y-2 min-h-[48px]">
          {cards.map((card) => (
            <SortableCard key={card.id} card={card} />
          ))}
        </div>
      </SortableContext>
    </div>
  )
}

function SortableCard({ card }: { card: Card }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: card.id })

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.4 : 1, // original position becomes semi-transparent
  }

  return (
    <div
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}
      className="bg-white rounded-md p-3 shadow-sm cursor-grab active:cursor-grabbing"
    >
      <KanbanCard card={card} />
    </div>
  )
}

Move Utilities

function moveCardBetweenColumns(
  board: BoardState,
  cardId: CardId,
  fromColId: ColumnId,
  toColId: ColumnId,
  overCardId: CardId | ColumnId
): BoardState {
  const fromCol = board.columns[fromColId]
  const toCol = board.columns[toColId]
  const newFromIds = fromCol.cardIds.filter((id) => id !== cardId)

  const insertIndex = isColumnId(overCardId)
    ? toCol.cardIds.length
    : toCol.cardIds.indexOf(overCardId as CardId)

  const newToIds = [...toCol.cardIds]
  newToIds.splice(insertIndex, 0, cardId)

  return {
    ...board,
    columns: {
      ...board.columns,
      [fromColId]: { ...fromCol, cardIds: newFromIds },
      [toColId]: { ...toCol, cardIds: newToIds },
    },
  }
}

State Persistence

To save order after reload — sync with backend after each dragEnd:

const mutation = useMutation({
  mutationFn: (update: BoardUpdatePayload) =>
    api.patch('/boards/main', update),
  onError: (_, __, context) => {
    // Rollback on error
    setBoard(context!.previousBoard)
    toast.error('Failed to save changes')
  },
})

function handleDragEnd(event: DragEndEvent) {
  // ... move logic
  const previousBoard = board
  const newBoard = applyDragEnd(board, event)
  setBoard(newBoard) // optimistic update

  mutation.mutate(
    { cardId: event.active.id, columnId: targetColId, position: newPosition },
    { context: { previousBoard } }
  )
}

Sorting Columns

Columns themselves can be dragged — columns are wrapped in SortableContext at board level:

<SortableContext
  items={board.columnOrder}
  strategy={horizontalListSortingStrategy}
>
  {board.columnOrder.map((colId) => (
    <SortableColumn key={colId} column={board.columns[colId]} ... />
  ))}
</SortableContext>

Determining what's dragged (card or column) — through data type in active.data.current.

What We Do

Design data structure for specific task (could be task board, sales funnel, content editor). Set up DnD with touch and keyboard support, implement live preview via DragOverlay, connect API sync with optimistic updates and rollback on error.

Timeframe: basic Kanban board — 3–4 days. With column sorting, nested tasks, and offline sync — 6–8 days.