Domphy

Animations

The animations() plugin adds CSS transition animations to sort and transfer operations. Items slide into their new positions after a drop rather than snapping instantly.

Basic Setup

Import animations and add it to plugins:

import { dragDrop, animations } from "@domphy/dnd"
import { toState } from "@domphy/core"
import { themeColor, themeSpacing } from "@domphy/theme"

const items = toState([
  { id: 1, label: "First" },
  { id: 2, label: "Second" },
  { id: 3, label: "Third" },
])

const SortableList = {
  ul: (l) =>
    items.get(l).map((item) => ({
      li: item.label,
      _key: item.id,
      style: {
        padding: themeSpacing(3),
        marginBottom: themeSpacing(2),
        backgroundColor: (cl) => themeColor(cl, "shift-2"),
        borderRadius: themeSpacing(2),
        cursor: "grab",
        userSelect: "none",
      },
    })),
  $: [dragDrop(items, { plugins: [animations()] })],
  style: { listStyle: "none", padding: "0" },
}

No extra CSS is required. animations() uses the Web Animations API internally and cleans up after itself.

Configuration

animations({
  duration: 200,           // ms — animation length (default: 150)
  easing: "ease-in-out",   // any WAAPI easing string (default: "ease-in-out")
})

Examples:

animations({ duration: 100, easing: "ease" })
animations({ duration: 300, easing: "cubic-bezier(0.22, 1, 0.36, 1)" })
animations({ duration: 50, easing: "linear" })

Animations in Multi-Container

Apply animations() to each list independently — each container manages its own animation:

const GROUP = "board"

const ListA = {
  ul: (l) => stateA.get(l).map((item) => ({ li: item.label, _key: item.id })),
  $: [dragDrop(stateA, { group: GROUP, plugins: [animations()] })],
}

const ListB = {
  ul: (l) => stateB.get(l).map((item) => ({ li: item.label, _key: item.id })),
  $: [dragDrop(stateB, { group: GROUP, plugins: [animations()] })],
}

Lists without animations() snap instantly; lists with it animate.

Combining Plugins

plugins is an array — pass multiple plugins in any order:

import { dragDrop, animations, dropOrSwap } from "@domphy/dnd"

dragDrop(items, {
  plugins: [
    animations({ duration: 180 }),
    dropOrSwap(),
  ],
})

Visual Feedback with CSS Classes

animations() handles the reorder transition. For immediate visual feedback during the drag, use the class config options. These are applied directly to DOM elements by FormKit, so they must live in a stylesheet:

dragDrop(items, {
  draggingClass: "item-dragging",          // the item being dragged
  dropZoneClass: "item-drop-target",       // item currently hovered over
  dropZoneParentClass: "list-drop-active", // parent list being hovered over
  plugins: [animations()],
})
const sheet = document.createElement("style")
sheet.textContent = `
  .item-dragging     { opacity: 0.4; }
  .item-drop-target  { outline: 2px dashed currentColor; outline-offset: -2px; }
  .list-drop-active  { background: rgba(0,0,0,.04); border-radius: 8px; }
`
document.head.appendChild(sheet)

Synthetic Drag Classes

On touch and pointer devices, FormKit uses a synthetic drag instead of the native HTML5 drag API. A separate set of class options controls the appearance during synthetic drags:

dragDrop(items, {
  synthDraggingClass: "synth-dragging",
  synthDropZoneClass: "synth-drop-target",
  synthDropZoneParentClass: "synth-list-active",
  plugins: [animations()],
})

Use the same CSS class names for both native and synthetic if you want consistent feedback regardless of input device:

const CLS = "item-dragging"

dragDrop(items, {
  draggingClass: CLS,
  synthDraggingClass: CLS,
  plugins: [animations()],
})

Respecting Reduced Motion

Check prefers-reduced-motion and skip the plugin when the user has requested it:

const prefersReducedMotion =
  window.matchMedia("(prefers-reduced-motion: reduce)").matches

dragDrop(items, {
  plugins: prefersReducedMotion ? [] : [animations({ duration: 150 })],
})