Skip to content

Group Validation

By default a zone only becomes accessible when all dragged items match its groups (groupMatch: 'every'). Set groupMatch: 'some' to allow the zone even when only part of the selection is compatible — invalid items are handled automatically.

Try multi-selecting both frontend and backend tasks, then dropping them on each zone to see the difference.

groupMatch option

ts
makeDroppable(zoneRef, {
  groups: ['frontend'],
  groupMatch: 'every', // default — ALL items must match
});

makeDroppable(zoneRef, {
  groups: ['frontend'],
  groupMatch: 'some',  // zone accessible if ANY item matches
});
groupMatchZone accessible when dragging mixed selection?What lands on drop?
'every' (default)No — zone stays blockedN/A
'some'Yes — zone is allowedOnly compatible items (invalid silently filtered)

Default behavior — silent filter

Without an onValidate handler, incompatible items are simply dropped. Only items whose groups match the zone's groups land on drop:

ts
// Zone accepts 'frontend'. User drags [frontend-task, backend-task].
// → Zone is accessible (some = frontend-task matches)
// → onDrop receives only [frontend-task]

makeDroppable(zoneRef, {
  groups: ['frontend'],
  groupMatch: 'some',
  events: {
    onDrop(e) {
      // e.draggedItems contains ONLY the compatible items
      e.draggedItems.forEach((di) => {
        const id = di.data as string;
        taskZone.value[id] = 'frontend-queue';
      });
    },
  },
});

onValidate — async control before drop

Add onValidate to intercept the mismatch and decide what to do. The handler fires before onDrop and can return a Promise — the drag state becomes 'pending' while you wait (e.g. for a user to confirm in a modal).

ts
makeDroppable(zoneRef, {
  groups: ['frontend'],
  groupMatch: 'some',
  events: {
    onValidate(e) {
      // e.validItems   — items matching zone groups
      // e.invalidItems — items that don't match

      return new Promise((resolve) => {
        showModal({
          message: `${e.invalidItems.length} item(s) don't match. What should we do?`,
          onDropCompatibleOnly: () => resolve(undefined),          // default: drop validItems only
          onDropAll:            () => resolve([...e.validItems, ...e.invalidItems]), // drop everything
          onCancel:             () => resolve(false),              // decline — drag stays active
        });
      });
    },

    onDrop(e) {
      // Receives exactly the items resolved by onValidate
      e.draggedItems.forEach((di) => { /* ... */ });
    },
  },
});

Return values of onValidate

ReturnEffect
void / undefinedDrop only validItems (same as no handler)
falseDecline — drag stays active, user can try another zone
IDragItem[]Drop exactly this array (you decide which items and in what order)
Promise<...>Async — drag state becomes 'pending' while awaiting; resolves to any of the above

Full type signature

ts
import type { IDragItem, IDropValidateEvent } from '@vue-dnd-kit/core';

interface IDropValidateEvent {
  validItems: IDragItem[];
  invalidItems: IDragItem[];
  dropZone: IDropZoneContext | undefined;
  hoveredDraggable?: IHoveredDraggableContext;
}

// in IDroppableEvents:
onValidate?: (
  event: IDropValidateEvent
) => void | false | IDragItem[] | Promise<void | false | IDragItem[]>;

Released under the MIT License.