Skip to main content

DDGhosts&Scripting

This guide covers the two ways you extend DragDrop beyond the defaults: customizing the ghost (the proxy that represents the payload while it is airborne) and driving drags programmatically through Scriptable mode.

Custom ghosts

Without a BuildGhost, the drag falls back to a plain plate listing the payload's tags. Supply BuildGhost on the source to render your own:

DragDrop.RegisterSource(slot, {
	GetPayload = function() return { Tags = { "Item" }, Guid = guid } end,
	BuildGhost = function(payload, source)
		local icon = Instance.new("ImageLabel")
		icon.Size = UDim2.fromScale(1, 1) -- fill the proxy (matches the source's footprint)
		icon.BackgroundTransparency = 1
		icon.Image = iconFor(payload.Guid)
		icon.ImageTransparency = 0.1
		icon.Rotation = 4 -- appearance is entirely yours
		return icon
	end,
})

The contract:

  • The returned GuiObject is parented into a proxy frame sized to the source's on-screen footprint. Return something with Size = UDim2.fromScale(1, 1) to match the source; give it an explicit offset Size to define its own.
  • Appearance belongs to BuildGhost. DragDrop never scales, tilts, or recolors the proxy. It owns placement only: keeping the grab point under the cursor, and springing between slots in grab mode.
  • The instance is destroyed when the drag ends. Don't reuse it across drags.

By default the proxy lives in an internal high-DisplayOrder ScreenGui. To host it yourself (custom layering, a specific ScreenGui), set Config.GhostContainer to any LayerCollector:

DragDrop.Configure({ GhostContainer = myOverlayScreenGui })

To go back to the internal container (or restore any other default), call DragDrop.ResetConfig()Configure can't clear an optional key, since nil values vanish from Luau tables.

Scriptable mode

SetInputMode("Scriptable") disables all automatic input backends. Drags then start and move only through BeginDrag and the DragHandle it returns — the same way you put a Camera into Scriptable and drive it by hand. Use it for cutscenes, guided tutorials, replays, and automated tests.

DragDrop.SetInputMode("Scriptable")

local drag = DragDrop.BeginDrag(sourceSlot)      -- returns a DragHandle (or nil if refused)
if drag then
	drag:SetHoveredTarget(destinationSlot)        -- explicit hover (no hit-test needed)
	local landed = drag:Drop()                    -- true if it dropped on a valid target
end

Or drive it by screen position, letting the system hit-test for the target under the point:

local drag = DragDrop.BeginDrag(sourceSlot, startPosition) -- position => pointer-controlled
drag:MoveTo(cursorPosition)  -- follows the point and resolves hover via hit-test
drag:Drop()

The handle's methods become no-ops (mutating ones return false) once the drag has ended, so it is always safe to hold onto.

BeginDrag also works in Automatic mode without a position — that is how a menu "Move" button initiates a grab-mode drag the gamepad/keyboard backend then drives. With a position it always produces a pointer-controlled drag that the automatic backends leave alone.

DragHandle reference

Method Effect
MoveTo(position) Move the ghost to a screen point and hit-test for hover.
SetHoveredTarget(target) Set hover explicitly; returns false if the target is invalid/incompatible.
Drop() End on the current hover; false if there was no valid target (a revert).
Cancel() Revert this drag.
IsActive() Whether this handle still controls the live drag.
GetPayload() The payload this drag carries.

See also

Show raw api
{
    "functions": [],
    "properties": [],
    "types": [],
    "name": "DD Ghosts & Scripting",
    "desc": "This guide covers the two ways you extend [DragDrop](/api/DragDrop) beyond the\ndefaults: customizing the **ghost** (the proxy that represents the payload while\nit is airborne) and driving drags **programmatically** through Scriptable mode.\n\n## Custom ghosts\n\nWithout a `BuildGhost`, the drag falls back to a plain plate listing the\npayload's tags. Supply `BuildGhost` on the source to render your own:\n\n```lua\nDragDrop.RegisterSource(slot, {\n\tGetPayload = function() return { Tags = { \"Item\" }, Guid = guid } end,\n\tBuildGhost = function(payload, source)\n\t\tlocal icon = Instance.new(\"ImageLabel\")\n\t\ticon.Size = UDim2.fromScale(1, 1) -- fill the proxy (matches the source's footprint)\n\t\ticon.BackgroundTransparency = 1\n\t\ticon.Image = iconFor(payload.Guid)\n\t\ticon.ImageTransparency = 0.1\n\t\ticon.Rotation = 4 -- appearance is entirely yours\n\t\treturn icon\n\tend,\n})\n```\n\nThe contract:\n\n- The returned `GuiObject` is parented into a proxy frame sized to the source's\n  on-screen footprint. Return something with `Size = UDim2.fromScale(1, 1)` to\n  match the source; give it an explicit offset `Size` to define its own.\n- **Appearance belongs to `BuildGhost`.** DragDrop never scales, tilts, or\n  recolors the proxy. It owns placement only: keeping the grab point under the\n  cursor, and springing between slots in grab mode.\n- The instance is destroyed when the drag ends. Don't reuse it across drags.\n\nBy default the proxy lives in an internal high-`DisplayOrder` `ScreenGui`. To\nhost it yourself (custom layering, a specific `ScreenGui`), set\n`Config.GhostContainer` to any `LayerCollector`:\n\n```lua\nDragDrop.Configure({ GhostContainer = myOverlayScreenGui })\n```\n\nTo go back to the internal container (or restore any other default), call\n`DragDrop.ResetConfig()` — `Configure` can't clear an optional key, since nil\nvalues vanish from Luau tables.\n\n## Scriptable mode\n\n`SetInputMode(\"Scriptable\")` disables all automatic input backends. Drags then\nstart and move **only** through [BeginDrag](/api/DragDrop#BeginDrag) and the\n[DragHandle](/api/DragDrop#DragHandle) it returns — the same way you put a Camera\ninto `Scriptable` and drive it by hand. Use it for cutscenes, guided tutorials,\nreplays, and automated tests.\n\n```lua\nDragDrop.SetInputMode(\"Scriptable\")\n\nlocal drag = DragDrop.BeginDrag(sourceSlot)      -- returns a DragHandle (or nil if refused)\nif drag then\n\tdrag:SetHoveredTarget(destinationSlot)        -- explicit hover (no hit-test needed)\n\tlocal landed = drag:Drop()                    -- true if it dropped on a valid target\nend\n```\n\nOr drive it by screen position, letting the system hit-test for the target under\nthe point:\n\n```lua\nlocal drag = DragDrop.BeginDrag(sourceSlot, startPosition) -- position => pointer-controlled\ndrag:MoveTo(cursorPosition)  -- follows the point and resolves hover via hit-test\ndrag:Drop()\n```\n\nThe handle's methods become no-ops (mutating ones return `false`) once the drag\nhas ended, so it is always safe to hold onto.\n\n`BeginDrag` also works in **Automatic** mode without a position — that is how a\nmenu \"Move\" button initiates a grab-mode drag the gamepad/keyboard backend then\ndrives. With a position it always produces a pointer-controlled drag that the\nautomatic backends leave alone.\n\n### DragHandle reference\n\n| Method | Effect |\n| --- | --- |\n| `MoveTo(position)` | Move the ghost to a screen point and hit-test for hover. |\n| `SetHoveredTarget(target)` | Set hover explicitly; returns `false` if the target is invalid/incompatible. |\n| `Drop()` | End on the current hover; `false` if there was no valid target (a revert). |\n| `Cancel()` | Revert this drag. |\n| `IsActive()` | Whether this handle still controls the live drag. |\n| `GetPayload()` | The payload this drag carries. |\n\n---\n### See also\n\n- **[DD Getting Started](/api/DD%20Getting%20Started)** — registering sources and targets.\n- **[DD Input Devices](/api/DD%20Input%20Devices)** — per-device gestures and the switch-to-revert rule.",
    "source": {
        "line": 102,
        "path": "lib/dragdrop/src/Docs/DD_Ghosts_And_Scripting.luau"
    }
}