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

dragee

v2.0.1

Published

Dragee(drag&drop) library

Readme

Dragee

npm version bundle size license

Drag-and-drop with movement constraints, sortable lists, drop trays and scopes. No dependencies.

Documentation & Demos | GitHub

Installation

npm install dragee
import { Draggable, Point } from 'dragee'

CommonJS: const { Draggable } = require('dragee').

Browser support

Chrome / Edge 90+, Firefox 86+, Safari 15+ (iOS 15+), required for subclassing EventTarget and the signal listener option. Older browsers: dragee 1.x.

Quick Start

import { Draggable } from 'dragee'

new Draggable(document.getElementById('my-element'))

Positions are measured relative to the element's offset parent.

Draggable

Makes an element draggable.

import { Draggable, Point } from 'dragee'

const draggable = new Draggable(element, {
  container: parentElement,
  position: new Point(100, 50),
  bound: (point) => new Point(point.x, Math.max(0, point.y)),
  handler: '.drag-handle',
  on: {
    'drag:end': ({ draggable }) => console.log('dropped at', draggable.position)
  }
})

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | container / parent | Element | element.offsetParent | Element that positions are measured from, from its outer border edge. Any ancestor works, including one with position: static | | position | Point | current place | Initial position | | bound | (point, size) => Point | none | Function that limits where the element can move (see Bounding) | | bounding | Bound | none | The same as bound, as a Bound instance | | handler | Element | string | the element | Part of the element (or a CSS selector) the drag starts from | | dragStartThreshold | number | 0 | Pixels the pointer must move before the drag starts | | touchDraggingThreshold | number | 0 | A touch that moves within this many milliseconds is treated as scrolling | | nativeDragAndDrop | boolean | false | Use the HTML5 Drag and Drop API | | emulateNativeDragAndDropOnTouch | boolean | false | Emulate native drag and drop on touch devices with a moving copy of the element | | copyStyles | (source, copy) => void | copies computed styles | How the touch emulation copy gets its styles | | dragOverThrottleDuration | number | 16 | Throttle (ms) for native dragover handling | | considerTransformOffset | boolean | false | Measure the position with getBoundingClientRect, including CSS transforms | | scrollRootContainer | Element | container | Scrolling of parents up to this element moves the dragged element along | | shouldRemoveZeroTranslate | boolean | false | Remove translate3d(0, 0, 0) at the initial position | | stopPropagationOnDragStart | boolean | false | Stop propagation of the event that starts the drag | | domEvents | boolean | true | Dispatch bubbling dragee:* DOM events | | scope | Scope | current scope | Scope to join (see Scope) | | on | object | — | Listeners, e.g. { 'drag:start': fn } |

An input, textarea, select or contenteditable element inside a draggable does not start a drag. Of nested draggables only the innermost one starts.

Events

| Event | Data | DOM event | When | | --- | --- | --- | --- | | drag:start | { draggable } | dragee:start | The drag starts (after dragStartThreshold). Cancelable | | drag:move | { draggable } | dragee:move | The position changes during the drag | | drag:release | { draggable } | dragee:release | The draggable is released, before it is placed. Cancelable | | drag:end | { draggable } | dragee:end | The drag is over |

DOM events bubble from the element:

container.addEventListener('dragee:end', ({ draggable }) => {
  console.log('dropped', draggable.element)
})

event.cancel() on drag:start cancels the drag:

draggable.on('drag:start', (event) => {
  if (isLocked) event.cancel()
})

event.cancel() on drag:release skips pinning the draggable at the drop position:

draggable.on('drag:release', (event) => {
  event.cancel()
  draggable.pinPosition(snapToGrid(draggable.position), { duration: 200 })
})

List and Scope place their draggables in a drag:release listener and skip an already canceled event.

Methods

| Method | Signature | Description | | --- | --- | --- | | move | (point, { duration?, silent? }) | Move to point, animated over duration ms (default 0). Emits drag:move unless silent | | pinPosition | (point, { duration?, silent? }) | Move and remember point as the rest position (pinnedPosition). Silent by default | | setPosition | (point) | Move instantly, without animation or events | | resetPositionToInitial | () | Go back to initialPosition | | refreshPosition | () | Re-apply the current position | | remeasure | () | Re-read the element's place after the layout changed. A draggable at its initial position moves to the new one | | getPosition | () → Point | Current position | | getCenter | () → Point | Center of the element | | getSize | () → Point | Width and height | | getRectangle | () → Rectangle | Position and size | | cancelDragging | () | Stop the current drag | | refresh | () | Re-read the bounding, e.g. the size of a BoundToElement element | | destroy | () | Remove all listeners and leave its scope and trays |

Properties

| Property | Type | Description | | --- | --- | --- | | element | Element | The DOM element | | position | Point | Current position | | pinnedPosition | Point | Rest position | | initialPosition | Point | Position to go back to on reset | | isDragging | boolean | Whether it is being dragged | | trays | Tray[] | Trays it can be dropped into | | enable | boolean | Set to false to disable dragging |


Bounding

A bound limits where a draggable can move. Pass a Bound instance as bounding or a function as bound. The static .bounding(...args) returns the bound function of a new instance.

import { Draggable, BoundToElement, BoundToCircle, Point } from 'dragee'

new Draggable(el, {
  bounding: new BoundToElement(area, area)
})

new Draggable(el, {
  bound: BoundToCircle.bounding(new Point(200, 200), 100)
})

| Class | Constructor | Keeps the element | | --- | --- | --- | | Bound | () | anywhere (base class) | | BoundToRectangle | (rectangle) | inside a Rectangle | | BoundToElement | (element, container?) | inside an element; re-reads its size on refresh() | | BoundToLineX | (x, startY, endY) | on the vertical line x, between startY and endY | | BoundToLineY | (y, startX, endX) | on the horizontal line y, between startX and endX | | BoundToLine | (startPoint, endPoint) | on the segment between two points | | BoundToCircle | (center, radius) | on a circle | | BoundToArc | (center, radius, startAngle, endAngle) | on an arc; the angles can be functions |

A bound function takes the requested position and the element size and returns the allowed position:

new Draggable(el, {
  bound: (point, size) => new Point(Math.max(0, Math.min(300 - size.x, point.x)), point.y)
})

Sortable Lists

List

Sortable list or grid. A dragged item takes the slot within radius.

import { Draggable, List } from 'dragee'

const container = document.querySelector('.grid')
const draggables = Array.from(container.querySelectorAll('.item'), (item) => new Draggable(item, { container }))

const list = new List(draggables, { container, radius: 40, reorderOnChange: true })

list.on('list:change', ({ draggable }) => console.log('order changed by', draggable.element))

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | radius | number | 30 | How close (px) a dragged item must come to a slot to take it | | timeEnd | number | 200 | Animation ms of the dropped item settling into its slot | | timeExchange | number | 400 | Animation ms of the other items moving aside | | reorderOnChange | boolean | false | Also reorder the DOM elements after a change | | container | Element | — | Re-lay out the items when this element is resized | | sorting | (a, b) => number | by y, then x | Order of the items | | getDistance | (p1, p2) => number | Euclidean | Distance used with radius | | domEvents | boolean | true | Dispatch bubbling dragee:* DOM events | | on | object | — | Listeners |

Events

| Event | Data | DOM event | When | | --- | --- | --- | --- | | list:change | { list, draggable } | dragee:list-change | A drop changed the order; draggable is the dragged item | | list:reordered | { list, draggable } | dragee:list-reordered | The DOM elements were reordered (with reorderOnChange) |

DOM events bubble from the dragged item's element; event.list is the list.

Methods

| Method | Signature | Description | | --- | --- | --- | | add | (draggable \| draggable[]) | Add items | | remove | (draggable \| draggable[]) | Remove items and close the gap | | clear | () | Remove all items | | reset | () | Move all items back to their initial positions | | refresh | () | Call refresh() on all items | | getSortedDraggables | () → Draggable[] | Items in their current order | | getCurrentPinnedPositions | () → Point[] | The same as positions | | destroy | () | Destroy the list and its draggables |

Properties

| Property | Type | Description | | --- | --- | --- | | draggables | Draggable[] | Items, in the order they were given | | positions | Point[] | Rest positions of draggables. Set it to restore a saved layout; the wrong length throws a RangeError | | enable | boolean | Set to false to disable dragging of all items | | swappingDisabled | boolean | Set to true to stop items from swapping |

BubblingList

Sortable list along one axis. Items may differ in size and are laid out one after another from the first item. Extends List; radius is not used.

import { Draggable, BubblingList } from 'dragee'

const container = document.querySelector('.list')
const draggables = Array.from(container.querySelectorAll('.item'), (item) => new Draggable(item, { container }))

new BubblingList(draggables, { container, reorderOnChange: true })
new BubblingList(horizontalDraggables, { axis: 'x', gap: 8 })

| Option | Type | Default | Description | | --- | --- | --- | --- | | axis | 'y' | 'x' | 'y' | 'y' — vertical, 'x' — horizontal | | gap | number | detected | Space between items; detected from the first two items if not set | | verticalGap | number | — | Alias for gap |

The gap property gets and sets the gap.


Tray

Drop area that lays out its draggables with a positioning strategy. A draggable leaves the tray when it is dragged. The layout is updated when the page layout changes.

import { Tray, FloatLeftStrategy } from 'dragee'

const tray = new Tray(trayElement, draggables, {
  strategy: new FloatLeftStrategy(() => tray.getRectangle(), { radius: 80 })
})

tray.on('tray:add', ({ draggable }) => console.log('added', draggable.element))

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | strategy | Strategy | FloatLeftStrategy | How the draggables are laid out | | catchDraggable | (tray, draggable) => boolean | center inside and smaller than the tray | Whether a dropped draggable is taken | | bound | (point, size) => Point | inside the tray | Where a dropped draggable may land | | timeEnd | number | 200 | Animation ms of a dropped draggable moving into place | | timeExchange | number | 400 | Animation ms of the other draggables moving aside | | container / parent | Element | element.offsetParent | Element that positions are measured from, from its outer border edge. Any ancestor works, including one with position: static | | domEvents | boolean | true | Dispatch bubbling dragee:* DOM events | | scope | Scope | current scope | Scope to join | | on | object | — | Listeners |

Events

| Event | Data | DOM event | When | | --- | --- | --- | --- | | tray:beforeAdd | { tray, draggable } | dragee:tray-before-add | Before a draggable is added. Cancelable | | tray:add | { tray, draggable } | dragee:tray-add | A draggable was added | | tray:remove | { tray, draggable } | dragee:tray-remove | A draggable left the tray |

event.cancel() on tray:beforeAdd refuses the draggable; a dropped one returns to its initial position:

tray.on('tray:beforeAdd', (event) => {
  if (tray.innerDraggables.length >= 5) event.cancel()
})

DOM events bubble from the tray element:

board.addEventListener('dragee:tray-add', ({ tray, draggable }) => {
  console.log(draggable.element, 'moved to', tray.element)
})

Methods

| Method | Signature | Description | | --- | --- | --- | | add | (draggable, { duration? }) | Put a draggable into the tray, animated over duration ms (default 0) | | remove | (draggable) | Take a draggable out of the tray | | accept | (draggable) | Allow a draggable to be dropped into the tray | | releaseDraggable | (draggable) | Stop accepting a draggable | | reset | () | Take all draggables out | | refresh | () | Lay out the draggables again | | getSortedDraggables | () → Draggable[] | Draggables inside, in order | | getRectangle / getPosition / getSize | () | The tray's rectangle, position and size | | destroy | () | Stop watching the draggables and leave all scopes |

Properties

| Property | Type | Description | | --- | --- | --- | | element | Element | The tray element | | innerDraggables | Draggable[] | Draggables inside the tray | | draggables | Draggable[] | Draggables that can be dropped into it | | container | Element | Element that positions are measured from |


Positioning Strategies

A strategy positions the draggables of a tray. Constructor: (rectangle, options), where rectangle is a Rectangle or a function returning one.

FloatLeftStrategy

Places draggables like CSS float: left: in rows from the top-left corner.

new FloatLeftStrategy(() => tray.getRectangle(), {
  radius: 80,
  paddingTopLeft: new Point(10, 10),
  yGapBetweenDraggables: 5
})

| Option | Type | Default | Description | | --- | --- | --- | --- | | radius | number | 80 | How close a dropped draggable must come to a place to take it | | paddingTopLeft | Point | (0, 0) | Space to the left of each draggable and above the first row | | paddingBottomRight | Point | (0, 0) | Space to the right of and below each draggable | | yGapBetweenDraggables | number | 0 | Vertical gap between rows | | getDistance | (p1, p2) => number | Euclidean | Distance used with radius | | getPosition | (draggable) => Point | draggable.position | Point of a draggable used for the distance | | removable | boolean | true | Send draggables that don't fit at the bottom back to their initial position |

FloatRightStrategy

Like float: right, from the top-right corner. Extends FloatLeftStrategy; instead of the paddings above it takes paddingTopRight (default (5, 5)) and paddingBottomLeft (default (0, 0)).

NotCrossingStrategy

Keeps draggables at their drop position without overlapping.

new NotCrossingStrategy(() => tray.getRectangle())

Scope

A draggable can be dropped only into trays of its scope. Instances belong to defaultScope unless created inside scope() or given the scope option.

scope()

Instances created inside the callback join the returned scope. Nested calls use the innermost scope.

import { scope, Draggable, Tray } from 'dragee'

let columns
const board = scope(() => {
  const cards = cardElements.map((el) => new Draggable(el))
  columns = columnElements.map((el) => new Tray(el, cards))
})

board.on('scope:change', () => console.log(board.positions))

Instances created after the callback returns join with the scope option; trays must accept() them:

const card = new Draggable(el, { scope: board })
columns.forEach((column) => column.accept(card))

Scope

new Scope(draggables, trays, { timeEnd }) creates a scope directly.

| Option | Type | Default | Description | | --- | --- | --- | --- | | timeEnd | number | 400 | Animation ms of a refused draggable going back to its initial position |

| Method | Description | | --- | --- | | addDraggable(draggable) | Move a draggable into this scope from its previous one | | addTray(tray) | Move a tray into this scope from its previous one | | reset() | Empty all trays | | refresh() | Refresh all draggables and trays |

| Property | Type | Description | | --- | --- | --- | | draggables | Draggable[] | Draggables of the scope | | trays | Tray[] | Trays of the scope | | positions | number[][] | For each tray, the indexes in draggables of the draggables inside. Set it to restore a saved layout; the wrong length throws a RangeError |

| Event | Data | When | | --- | --- | --- | | scope:change | { scope, draggable } | A draggable that belongs to trays was dropped |

Also exported: defaultScope and scopes (all scopes).


Geometry

Point

A 2D point or vector: new Point(x, y).

| Method | Returns | | --- | --- | | add(p) / sub(p) | Sum / difference with another point | | mult(k) | The point multiplied by a number | | negative() | The point with both coordinates negated | | compare(p) | Whether both coordinates are equal | | clone() | A copy | | Point.elementOffset(element, parent?) | Position of an element relative to the outer border edge of an ancestor parent, without transforms | | Point.elementBoundingOffset(element, parent?) | The same, from getBoundingClientRect (includes transforms) | | Point.elementSize(element) | Width and height of an element |

Rectangle

new Rectangle(position, size), both Points.

| Method | Returns | | --- | --- | | getP1() … getP4() | Corners: top-left, top-right, bottom-right, bottom-left | | getCenter() | The center | | getSquare() | The area | | includePoint(p) / includeRectangle(r) | Whether the point / rectangle is inside | | or(r) / and(r) | The bounding rectangle of both / their intersection (or null) | | Rectangle.fromElement(element, parent?, considerTranslate?) | The rectangle of an element |

Distance functions

| Function | Returns | | --- | --- | | getDistance(p1, p2) | Euclidean distance | | getXDifference(p1, p2) / getYDifference(p1, p2) | Distance along one axis | | transformedSpaceDistanceFactory({ x, y }) | A distance function with weighted axes | | indexOfNearestPoint(points, point, radius, getDistance?) | Index of the nearest point within radius, or -1 |


Events

Draggable, List, BubblingList, Tray and Scope are EventTargets. Event data is available as event properties, e.g. ({ draggable }) => {}, and in event.detail. Cancelable events are canceled with event.cancel() or event.preventDefault().

| Method | Description | | --- | --- | | on(name, fn, options?) | Add a listener; returns a function that removes it | | off(name, fn) | Remove a listener (unsubscribe is an alias) | | once(name, fn) | Add a listener for the next event only |

addEventListener and its options work too:

const controller = new AbortController()
draggable.on('drag:move', onMove, { signal: controller.signal })
controller.abort()

Constructors take listeners in the on option: new Draggable(el, { on: { 'drag:end': fn } }).

EventEmitter and DrageeEvent

EventEmitter is the base class of all dragee classes. A subclass gets the same event API:

import { EventEmitter } from 'dragee'

class Slider extends EventEmitter {
  constructor(element, options = {}) {
    super(options)
    this.element = element
  }

  setValue(value) {
    const event = this.emitWithDomEvent(this.element, 'slider:change', 'slider-change', { slider: this, value }, { cancelable: true })
    if (!event.canceled) this.value = value
  }
}

const slider = new Slider(element, { on: { 'slider:change': ({ value }) => console.log(value) } })

| Method | Description | | --- | --- | | emit(name, data?, { cancelable? }) | Dispatch a DrageeEvent; returns it | | emitWithDomEvent(element, name, domName, data?, { cancelable? }) | Also dispatch a bubbling DOM event domName from element, unless the domEvents option is false. Canceling either event cancels the returned one |

The constructor stores options as this.options and adds the listeners of its on option.

DrageeEvent is a CustomEvent with the data in event.detail and as event properties, so data keys must not be Event properties such as type or target. event.cancel() and event.canceled stand for preventDefault() and defaultPrevented.


CSS Classes

| Class | Set when | | --- | --- | | dragee-active | The element is being dragged | | dragee-disable | enable is false | | dragee-placeholder | The original element during a native (or emulated) drag | | dragee-native-emulation | The moving copy during touch emulation |


Migrating from 1.x

  • Browsers: Chrome / Edge 90+, Firefox 86+, Safari 15+.
  • Listeners get an event: tray.on('tray:add', ({ draggable }) => {}) instead of target.on('target:add', (draggable) => {}).
  • Target is now Tray, also in draggable.trays, scope.trays, scope.addTray() and the tray:* / dragee:tray-* events.
  • Options objects: pinPosition(point, { duration: 200 }), move(point, { silent: true }) and tray.add(draggable, { duration: 200 }) instead of positional arguments.
  • timeExcange is now timeExchange. The old name is ignored.
  • dragEndAction is removed. Cancel drag:release instead.
  • Removed: prependOn, resetOn, resetEmitter, interrupt, Draggable.emitter and Target.emitter. Use scope() or the scope option.
  • Listeners work like addEventListener: the same function added twice is called once, and an error in one listener doesn't stop the others.
  • A draggable refused by a tray returns to its initial position.
  • Form fields inside a draggable (input, textarea, select, contenteditable) no longer start a drag.
  • Errors are Error objects (RangeError for positions of the wrong length) instead of strings.
  • Import from dragee only: deep imports such as dragee/src/… or dragee/dist/… fail.

License

ISC