How Terrarium Drag and Drop Works: Native Pointer Events and Closure Scope Explained

Terrarium implements drag-and-drop functionality using native pointer events and a closure-based helper function that tracks cursor deltas to reposition plant images via inline CSS styles.

The drag-and-drop system in the Terrarium project from microsoft/Web-Dev-For-Beginners demonstrates fundamental DOM manipulation techniques without relying on external libraries. By leveraging standard browser APIs and JavaScript closures, the project enables users to drag fourteen different plant images into a virtual glass jar using lightweight, vanilla JavaScript.

Registering Draggable Elements in script.js

In script.js, the application initializes drag behavior by invoking the dragElement helper for every plant image in the selection panel. The script targets fourteen unique elements identified as plant1 through plant14 that reside in the left and right containers defined in index.html.

// Register each plant image as draggable
dragElement(document.getElementById('plant1'));
// … repeated for plant2 through plant14

(see script.js lines 3‑16)

The Closure-Based Architecture

The dragElement function creates a private scope for each plant element by declaring four position variables inside its function body. This closure pattern ensures that pos1, pos2, pos3, and pos4 remain accessible to the nested event handlers even after dragElement returns, allowing each plant to maintain independent state during drag operations.

function dragElement(terrariumElement) {
    let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
    // Event handlers defined here retain access to these variables
}

(see script.js lines 23‑28)

Because every plant receives its own closure instance, users can drag multiple plants independently without position calculations interfering between elements.

Handling Pointer Events

The implementation uses the Pointer Events API to handle input from mice, touchscreens, and styluses through a unified interface.

Starting the Drag Operation

When a user presses on a plant image, the onpointerdown handler records the initial cursor coordinates and registers document-level listeners for movement and release events.

terrariumElement.onpointerdown = function(e) {
    e.preventDefault();
    pos3 = e.clientX;   // initial cursor X
    pos4 = e.clientY;   // initial cursor Y
    document.onpointermove = elementDrag;
    document.onpointerup   = stopElementDrag;
};

(see script.js lines 29‑41)

Calculating Movement Deltas

During the drag operation, the elementDrag function computes the distance between the previous cursor position and the current one. It calculates the horizontal delta (pos1) and vertical delta (pos2), then subtracts these values from the element's current offsetTop and offsetLeft properties to determine the new position.

function elementDrag(e) {
    pos1 = pos3 - e.clientX;   // horizontal delta
    pos2 = pos4 - e.clientY;   // vertical delta
    pos3 = e.clientX;          // update previous X
    pos4 = e.clientY;          // update previous Y

    terrariumElement.style.top  = terrariumElement.offsetTop  - pos2 + 'px';
    terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}

(see script.js lines 44‑56)

Cleaning Up Event Listeners

When the user releases the pointer, the stopElementDrag function removes the document-level listeners to prevent further unintended movement.

function stopElementDrag() {
    document.onpointerup   = null;
    document.onpointermove = null;
}

(see script.js lines 59‑63)

HTML Structure Supporting Drag Operations

The index.html file provides the DOM structure that enables this functionality. Each plant image receives a unique identifier that script.js uses to attach event listeners, while CSS positioning allows the JavaScript to modify top and left properties freely.

<div id="plant-selection">
  <div id="left-container" class="container">
    <div class="plant-holder">
      <img class="plant" id="plant1" src="./images/plant1.png" alt="plant">
    </div>
    <!-- … additional left-side plants … -->
  </div>
</div>

Summary

  • Closure isolation: The dragElement function creates private scope for each plant, preventing interference between multiple draggable elements.
  • Native pointer events: The implementation uses onpointerdown, onpointermove, and onpointerup for cross-device compatibility without library dependencies.
  • Delta calculations: Position tracking relies on cursor coordinate differences (stored in pos1 through pos4) applied to offsetTop and offsetLeft properties.
  • Cleanup pattern: Document-level listeners are explicitly removed in stopElementDrag to prevent memory leaks and ghost dragging.
  • File locations: Core logic resides in 3-terrarium/solution/script.js with DOM elements defined in 3-terrarium/solution/index.html.

Frequently Asked Questions

How does Terrarium's drag and drop differ from the HTML5 Drag and Drop API?

Terrarium uses manual pointer event handling and absolute positioning rather than the native HTML5 Drag and Drop API. This approach provides direct control over the visual presentation during dragging and avoids the ghost image previews and limited styling options inherent in the standard API.

Why does the implementation use pointer events instead of mouse events?

Pointer events unify input handling across mice, touchscreens, pens, and other pointing devices. By using onpointerdown, onpointermove, and onpointerup, the Terrarium project supports both desktop and mobile interactions without requiring separate touch event handlers or feature detection code.

How does the closure pattern prevent interference between multiple plants?

Each invocation of dragElement creates a new execution context containing independent instances of pos1 through pos4. When event handlers execute, they reference their specific closure's variables through lexical scoping, ensuring that dragging plant1 does not affect the position calculations of plant2 or any other element.

Can this drag and drop implementation handle simultaneous multi-touch interactions?

While the closure architecture supports multiple independent elements, the current implementation attaches listeners to document using direct property assignment (document.onpointermove), which handles one pointer interaction at a time per document. For true multi-touch support, the code would need to track pointer IDs and use addEventListener with the pointerId property to distinguish between simultaneous touches.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →