Liking cljdoc? Tell your friends :D

<x-drop-zone>

A region that senses <x-drag-panel> elements passing over it, shows where one would land, and reports the drop.

The zone never moves a node and never decides anything on its own. Acceptance is declared with accepts and max; the landing is the application's job.

<x-drop-zone value="doing" accepts="task bug" label="In progress" max="5" animate-moves>
  <span slot="empty">Nothing in progress</span>
  <x-drag-panel kind="task" value="t-104" label="Rewrite the parser">
    <span slot="header">Rewrite the parser</span>
  </x-drag-panel>
</x-drop-zone>

Not to be confused with the drop area inside <x-file-upload>, which accepts files from the operating system. This zone accepts panels from the page.

Handling a drop

The detail describes the whole move as data: which panel (value), between which two zones (fromto), at what rank (index). Both endpoints are opaque strings, so nothing you hold needs to survive a re-render.

document.addEventListener('x-drop-zone-drop', (e) => {
  const { value, from, to, index, panel } = e.detail;
  // "move task `value` from `from` to `to`, at rank `index`"
  await submitWrite(value, to, index);
});

index is computed from the pointer against the measured positions of the zone's current panels — the one thing the component knows that your app would otherwise have to re-derive. A panel dragged within its own zone is excluded from the measurement, so dropping it back where it started yields its original index and you can cheaply no-op.

panel is the only element reference in the detail. It stays because the synchronous DOM path needs a node to move and it cannot be derived:

const siblings = [...zone.querySelectorAll(':scope > x-drag-panel')]
  .filter((c) => c !== panel);
zone.insertBefore(panel, siblings[index] || null);

The source zone element is deliberately not in the detail. Inside the handler it is panel.closest('x-drop-zone'); after that, from is what you want, and holding the element across a round trip is the bug from exists to prevent.

The in-flight window

For a server-authoritative board, reserve the position when you send the request and release it when the answer arrives:

document.addEventListener('x-drop-zone-drop', async (e) => {
  const zone = e.target;
  const { value, to, index, panel } = e.detail;

  zone.reserve(panel, index);        // panel footprint + zone caret and busy tint
  const ok = await submitWrite(value, to, index);
  if (ok) applyMove(zone, panel, index);
  zone.release();                     // clears all three
});

reserve() sets pending on the panel and pending + pending-index on the zone; release() clears them. Holding the zone across the await is safe — it is the element that survives a confirmation re-render, which is the same reason focus moves here on a keyboard drop.

Nothing in the layout moves during that window — the panel holds its box open as a footprint, and this zone does not reflow. Every real layout change happens once, when the truth arrives.

Reserving without an index gives the busy tint and no caret, which is the right rendering for an unordered bucket.

pending and pending-index remain fully author-settable if you would rather drive them yourself; reserve() and release() are sugar over exactly those attributes. Either way the component never infers that a drop was sent — it reserves only when you ask it to, having decided to send it.

A reservation does not count toward max: the panel has not arrived, and counting it would refuse a second drop the server may well accept.

Attributes

AttributeTypeDefaultDescription
valuestring""Opaque identity key for this zone — a status, a column id, a bucket name. Echoed as from / to in the drop detail.
acceptsstringabsentSpace-separated kinds. Absent accepts any panel — deliberately different from accepting nothing.
maxnumberabsentCapacity, counted over x-drag-panel children. A full zone renders the reject cue.
labelstring"Drop zone"Accessible name for the group, and the name used in narration.
disabledbooleanabsentNever accepts, never renders cues.
pendingbooleanabsentBusy tint and aria-busy. Author-set.
pending-indexnumberabsentCaret position during the in-flight window. A stale index parks the caret at the end rather than throwing.
animate-movesbooleanabsentAnimate children into place when the child list changes.

State attributes

AttributeValuesMeaning
data-drag-stateover | rejectA compatible / incompatible panel is hovering. Absent otherwise. Set by the component; style-only.

One valued attribute rather than two booleans, because the states are mutually exclusive.

Properties

PropertyTypeReflects
valuestringvalue
acceptsstringaccepts
maxnumbermax
labelstringlabel
disabledbooleandisabled
pendingbooleanpending
pendingIndexnumberpending-index
animateMovesbooleananimate-moves

max and pendingIndex read as null when their attribute is absent. Assigning null removes the attribute.

Events

All bubble and cross shadow boundaries. None is cancelable — the app owns the landing, so there is no default action to prevent.

EventDetailFires when
x-drop-zone-enter{ kind, value }A dragged panel enters, whether accepted or rejected.
x-drop-zone-leave{ kind, value }A dragged panel leaves, including when dropped elsewhere.
x-drop-zone-drop{ kind, value, from, to, index, panel }An accepted panel is released over the zone, by pointer or by Enter.

value is the panel's identity key; from and to are the source and destination zones' values; index is the rank within the destination; panel is the <x-drag-panel> element.

from is null when the panel was dragged from outside any zone — distinct from "", which means it came from a zone the author gave no value. The generated TypeScript types it string | null so the check is not optional.

enter and leave fire for rejected panels too, so you can distinguish hovered but refused from never hovered — useful for expanding a collapsed column when a panel is dragged over it. A rejected release fires no drop event; the panel emits x-drag-panel-drag-cancel with reason "no-zone" instead.

Methods

MethodSignatureEffect
reserve(panel: HTMLElement, index: number) => voidSets pending on panel and pending + pending-index on the zone. Pass a non-numeric index for busy-only.
release() => voidClears all three. Idempotent.

A zone holds one reservation. Calling reserve() again clears the previous panel's pending first, so a second drop cannot strand the first panel. A release() whose panel was destroyed by a re-render is a harmless no-op — the replacement never carried pending to begin with.

Slots

SlotContent
(default)The panels.
emptyEmpty-state content, shown only when the zone holds no panels.

Parts

PartElement
zoneOuter region. Carries the hover, reject and busy states.
caretInsertion caret.
emptyWrapper around the empty slot.

CSS custom properties

PropertyPurpose
--x-drop-zone-bgResting background.
--x-drop-zone-borderResting border.
--x-drop-zone-radiusCorner radius.
--x-drop-zone-paddingInner padding.
--x-drop-zone-gapGap between panels.
--x-drop-zone-min-heightMinimum height, so an empty zone stays a target.
--x-drop-zone-over-bgBackground while a compatible panel hovers.
--x-drop-zone-over-borderBorder while a compatible panel hovers.
--x-drop-zone-reject-bgBackground while an incompatible panel hovers.
--x-drop-zone-reject-borderBorder while an incompatible panel hovers.
--x-drop-zone-caret-colorCaret colour.
--x-drop-zone-caret-sizeCaret thickness.
--x-drop-zone-busy-bgTint while pending.

A zone is an inline surface and uses --x-color-surface.

Flow axis

The zone lays its panels out in a column by default. Restyling ::part(zone) into a row moves the caret onto the horizontal axis automatically — the axis is read from the computed flex-direction rather than assumed.

x-drop-zone::part(zone) {
  flex-direction: row;
  overflow-x: auto;
}

A single flow axis is assumed. A wrapping layout would need two-dimensional hit-testing and is not supported.

animate-moves

When present, the zone watches its own child list and animates panels from their previous positions whenever it changes. One mechanism covers drop confirmations, another user's change arriving over a websocket, and filter toggles.

This matters most for server-authoritative boards: without it, every confirmation produces a visible jump as the source footprint disappears and this zone grows, at unpredictable latency.

The animation runs through the Web Animations API, so the component never writes to the style attribute of your elements. It is disabled entirely under prefers-reduced-motion: reduce.

Accessibility

  • The host is role="group" with aria-label from label. Not role="list" — panels are not necessarily list items, and forcing the role would misdescribe most boards.
  • While pending, the host carries aria-busy="true".
  • The zone narrates into a visually hidden aria-live region: it announces itself as a keyboard candidate ("Rewrite the parser over In progress, zone 2 of 3"), and announces the resolution of a drop it received. Whether the move succeeded is read from the DOM — the panel is either a child now or it is not — so a server that refused is reported honestly.
  • Focus moves here when a keyboard drop is committed.

Motion

Under prefers-reduced-motion: reduce the hover transition and the pending caret pulse are static, and animate-moves is disabled. The caret still renders.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close