Drag-and-Drop API Integration

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_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    879
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    453

Implementing Drag-and-Drop API on a Website

The native Drag and Drop API in the browser is not the most pleasant interface to work with. The order of events is not obvious, dataTransfer behaves differently across browsers, and dragover needs to be prevented by default, otherwise drop won't work. However, it's built into the browser, supports dragging files from the OS, works without libraries, and doesn't bloat the bundle.

For complex sortable lists with touch support and animations — look at @dnd-kit/core. For file uploads and basic drag-and-drop — the native API is sufficient.

Main Events

The order of events when dragging:

dragstart → dragenter → dragover (every ~50ms) → drop → dragend

On the source element: dragstart, drag, dragend. On the target element: dragenter, dragover, dragleave, drop.

Draggable Element

function makeDraggable(element: HTMLElement, data: Record<string, string>): void {
  element.setAttribute('draggable', 'true')

  element.addEventListener('dragstart', (event: DragEvent) => {
    if (!event.dataTransfer) return

    // Set data for transfer
    for (const [type, value] of Object.entries(data)) {
      event.dataTransfer.setData(type, value)
    }

    // Operation type: copy | move | link
    event.dataTransfer.effectAllowed = 'move'

    // Custom drag ghost
    const ghost = element.cloneNode(true) as HTMLElement
    ghost.style.cssText = 'position:absolute;top:-9999px;opacity:0.8'
    document.body.appendChild(ghost)
    event.dataTransfer.setDragImage(ghost, 0, 0)
    setTimeout(() => document.body.removeChild(ghost), 0)

    element.classList.add('is-dragging')
  })

  element.addEventListener('dragend', () => {
    element.classList.remove('is-dragging')
  })
}

Drop Zone

function makeDropZone(
  zone: HTMLElement,
  onDrop: (data: string, event: DragEvent) => void,
  acceptType = 'text/plain'
): void {
  // Without preventDefault() here drop won't work
  zone.addEventListener('dragover', (event: DragEvent) => {
    if (!event.dataTransfer?.types.includes(acceptType)) return
    event.preventDefault()
    event.dataTransfer.dropEffect = 'move'
    zone.classList.add('drop-zone--active')
  })

  zone.addEventListener('dragleave', (event: DragEvent) => {
    // Check that the cursor actually left the zone (didn't enter a child element)
    if (!zone.contains(event.relatedTarget as Node)) {
      zone.classList.remove('drop-zone--active')
    }
  })

  zone.addEventListener('drop', (event: DragEvent) => {
    event.preventDefault()
    zone.classList.remove('drop-zone--active')
    const data = event.dataTransfer?.getData(acceptType)
    if (data) onDrop(data, event)
  })
}

File Upload via Drag

function makeFileDropZone(
  zone: HTMLElement,
  onFiles: (files: FileList) => void,
  accept?: string[]
): void {
  zone.addEventListener('dragover', (event: DragEvent) => {
    if (!event.dataTransfer?.types.includes('Files')) return
    event.preventDefault()
    event.dataTransfer.dropEffect = 'copy'
    zone.classList.add('drop-zone--active')
  })

  zone.addEventListener('dragleave', (event: DragEvent) => {
    if (!zone.contains(event.relatedTarget as Node)) {
      zone.classList.remove('drop-zone--active')
    }
  })

  zone.addEventListener('drop', (event: DragEvent) => {
    event.preventDefault()
    zone.classList.remove('drop-zone--active')

    const files = event.dataTransfer?.files
    if (!files?.length) return

    if (accept) {
      const filtered = Array.from(files).filter((f) =>
        accept.some((type) =>
          type.startsWith('.') ? f.name.endsWith(type) : f.type.startsWith(type.replace('*', ''))
        )
      )
      if (!filtered.length) return
      const dt = new DataTransfer()
      filtered.forEach((f) => dt.items.add(f))
      onFiles(dt.files)
    } else {
      onFiles(files)
    }
  })
}

Sortable List

Classic pattern — dragging cards to change order:

interface SortableItem {
  id: string
  element: HTMLElement
}

class SortableList {
  private items: SortableItem[] = []
  private draggedId: string | null = null

  constructor(
    private container: HTMLElement,
    private onChange: (ids: string[]) => void
  ) {}

  add(id: string, element: HTMLElement): void {
    element.setAttribute('draggable', 'true')
    element.dataset.id = id

    element.addEventListener('dragstart', (e: DragEvent) => {
      this.draggedId = id
      e.dataTransfer!.setData('text/plain', id)
      e.dataTransfer!.effectAllowed = 'move'
      element.classList.add('sortable--dragging')
    })

    element.addEventListener('dragend', () => {
      element.classList.remove('sortable--dragging')
      this.draggedId = null
      this.container.querySelectorAll('.sortable--over').forEach((el) =>
        el.classList.remove('sortable--over')
      )
    })

    element.addEventListener('dragover', (e: DragEvent) => {
      e.preventDefault()
      if (this.draggedId === id) return
      element.classList.add('sortable--over')

      // Insert dragged element before current
      const draggedEl = this.container.querySelector(`[data-id="${this.draggedId}"]`)
      if (draggedEl && draggedEl !== element) {
        const rect = element.getBoundingClientRect()
        const insertBefore = e.clientY < rect.top + rect.height / 2
        element.parentNode?.insertBefore(
          draggedEl,
          insertBefore ? element : element.nextSibling
        )
      }
    })

    element.addEventListener('dragleave', () => {
      element.classList.remove('sortable--over')
    })

    element.addEventListener('drop', (e: DragEvent) => {
      e.preventDefault()
      element.classList.remove('sortable--over')
      // Order already updated in dragover, here we notify the outside
      const newOrder = Array.from(
        this.container.querySelectorAll('[data-id]')
      ).map((el) => (el as HTMLElement).dataset.id!)
      this.onChange(newOrder)
    })

    this.items.push({ id, element })
    this.container.appendChild(element)
  }
}

React Hook for Drag-and-Drop

function useDraggable(id: string) {
  const [isDragging, setIsDragging] = useState(false)

  const dragHandlers = {
    draggable: true as const,
    onDragStart: (e: React.DragEvent) => {
      e.dataTransfer.setData('text/plain', id)
      e.dataTransfer.effectAllowed = 'move'
      setIsDragging(true)
    },
    onDragEnd: () => setIsDragging(false),
  }

  return { isDragging, dragHandlers }
}

function useDroppable(onDrop: (id: string) => void) {
  const [isOver, setIsOver] = useState(false)

  const dropHandlers = {
    onDragOver: (e: React.DragEvent) => {
      e.preventDefault()
      setIsOver(true)
    },
    onDragLeave: (e: React.DragEvent) => {
      if (!(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) {
        setIsOver(false)
      }
    },
    onDrop: (e: React.DragEvent) => {
      e.preventDefault()
      setIsOver(false)
      const id = e.dataTransfer.getData('text/plain')
      if (id) onDrop(id)
    },
  }

  return { isOver, dropHandlers }
}

Touch Devices

Native DnD on iOS doesn't work on most elements. For touch support, you need a polyfill (drag-touch) or library @dnd-kit/core, which uses the Pointer Events API and works on all devices. The choice depends on project requirements.

What's Included

Implementation of draggable elements and drop zones, sortable list (if needed), file upload via drag with type filtering, React hooks, CSS styles for drag states, solving the touch support issue.

Timeline: 1–2 days depending on scenario complexity and need for touch support.