Skip to content

UMG Inventory Slots, ListView/TileView, and Drag/Drop

Inventory UI is a view of inventory state, not the inventory itself. The slot widget can draw an icon and request a move, but the inventory owner decides whether the item actually moved.

At a glance

Use this for: inventory grids, scrolling bags, hotbars, shops, and drag/drop item moves - Main UI choices: manual slot widgets, ScrollBox, ListView, and TileView - Main boundary: widgets request actions; the inventory component mutates entries and broadcasts refreshes - Official docs: UScrollBox, UListView, UTileView, Create Drag & Drop Operation, and Creating Drag and Drop UI

The one-minute version

  • For a small fixed inventory or hotbar, manually create slot widgets in a grid/wrap layout. It is direct and easy to debug.
  • A ScrollBox is a simple scrollable panel of child widgets. It is good for a modest list, but it is not virtualized; if you add 200 child widgets, you made 200 child widgets.
  • A ListView is for larger or frequently refreshed lists. It owns a list of item objects and only creates enough visible entry widgets to display them.
  • A TileView is a ListView presented as uniformly sized tiles. Use it when the inventory is visually a grid but you still want list-view virtualization.
  • In ListView/TileView, the item object is the data object in the list. The entry widget is the temporary visual row/tile that draws one item.
  • Drag/drop should carry a payload such as slot index, item view object, or move request. Dropping should call the inventory owner, not directly edit widget variables.
  • Refresh from OnInventoryChanged. Do not rebuild inventory UI every Tick.

Pick the UI shape first

There are three common beginner shapes:

Shape Use when Watch out
Manual slot widgets Fixed grids, hotbars, equipment slots, or a bag small enough to inspect by hand. You must create, clear, and refresh every slot yourself.
ScrollBox A small or medium scrolling list where direct child widgets are fine. It does not virtualize entries, so every child you add exists.
ListView / TileView Large lists, shop lists, loot lists, save slots, or inventory grids that may grow. Entry widgets are reused; do not store authoritative item state inside them.

For a first fixed 20-slot bag, manual slots are usually the least confusing path:

WBP_Inventory
-> UniformGridPanel or WrapBox
-> create 20 WBP_InventorySlot widgets
-> each slot receives SlotIndex and current entry data

For a compact vertical loot list, a ScrollBox is enough:

RefreshLootList
-> Clear Children on LootScrollBox
-> for each loot entry:
       Create Widget WBP_LootRow
       WBP_LootRow.Setup(entry)
       Add Child to LootScrollBox

For hundreds of item rows or a grid that changes often, use ListView or TileView:

InventoryComponent changed
-> build/update item view objects
-> InventoryTileView.SetListItems(ItemViewObjects)

That is extra setup, but it avoids making and updating every possible tile when only a handful are visible.

Manual slots

Manual slots are normal for fixed slot inventories:

WBP_InventorySlot
    SlotIndex
    CurrentEntry
    Icon
    CountText
    EquippedMarker

The parent widget owns the list of slot widgets:

WBP_Inventory opens
-> get InventoryComponent from owning player/pawn/controller
-> bind to InventoryComponent.OnInventoryChanged
-> create one WBP_InventorySlot per slot index
-> refresh once from current entries

Each slot should receive enough data to draw itself, then send requests back:

Slot clicked
-> WBP_InventorySlot calls SlotClicked(SlotIndex) dispatcher
-> WBP_Inventory or PlayerController handles it
-> InventoryComponent.RequestUse(SlotIndex)

Do not make WBP_InventorySlot the only place that knows the item count. The inventory entry is the state. The slot widget is a renderer and command surface.

ScrollBox

A ScrollBox is just a scrollable collection of widgets. That is useful when you want a quick list and the number of visible plus off-screen widgets is small enough that creating all of them is fine.

Good ScrollBox uses:

  • a loot window with a few dozen rows;
  • quest entries in a small journal;
  • debug/admin lists;
  • a simple shop prototype;
  • a small inventory where every row really should exist as a widget.

Refresh it deliberately:

RefreshInventoryScrollBox
-> InventoryScrollBox.ClearChildren
-> for each InventoryEntry:
       Row = Create Widget WBP_InventoryRow
       Row.Setup(InventoryEntry, InventoryComponent)
       InventoryScrollBox.AddChild(Row)

The two common mistakes are forgetting ClearChildren, which duplicates rows, and rebuilding the whole scroll box from Tick or a binding. Use the inventory owner's change event instead.

ListView and TileView

ListView and TileView are different from a panel full of child widgets. They store item objects and generate entry widgets as needed.

The practical rule:

Item object = data the list is displaying
Entry widget = temporary visual widget used to display one item object

If a list has 300 items but only 8 rows fit on screen, Unreal does not need 300 entry widgets alive at once. It can reuse entry widgets as the user scrolls. That is why list views scale better, and also why they have a sharper trap: the entry widget is not permanent item storage.

Common Blueprint shape:

BP_InventoryItemViewObject
    SlotIndex
    ItemId
    Count
    Icon
    bEquipped

WBP_InventoryEntry
    implements User Object List Entry
    On List Item Object Set(ItemObject)
        Cast to BP_InventoryItemViewObject
        cache it for display/request use
        set icon/count/equipped visuals

The item view object can be a simple UObject Blueprint class or another object shape your project deliberately uses for UI data. The important part is that it identifies the inventory entry or slot. The inventory component still owns the real FInventoryEntry array.

Use ListView for rows:

Potion x4
Iron Key x1
Rifle Ammo x30

Use TileView for uniform grid tiles:

[Potion] [Key] [Ammo] [Empty]
[Sword ] [Boots] [Map ] [Empty]

TileView inherits the same item/entry distinction from ListView; it just lays entries out as equally sized tiles.

Item object vs entry widget

This distinction is the most important part of virtualized UI.

Thing What it is Safe to store there
Inventory entry struct Real runtime item state owned by the inventory component. Item ID, count, slot, durability, equipped state.
Item view object UI-facing object handed to ListView/TileView. Slot index, display data, stable item/entry identifier.
Entry widget Visible row/tile widget generated and reused by the list. Current display cache and click/drag handlers.

Do not store "this is the player's sword" only in the entry widget. When the user scrolls, that widget may later draw a potion. If the widget kept old state, you get wrong icons, wrong counts, or actions on the wrong slot.

When the inventory changes:

InventoryComponent mutates Entries
-> Call OnInventoryChanged
-> WBP_Inventory rebuilds or updates item view objects
-> ListView/TileView refreshes visible entry widgets

When one entry widget is clicked:

Entry widget has ItemViewObject.SlotIndex = 4
-> call InventoryComponent.RequestUseSlot(4)

That preserves authority: UI knows which thing the user selected; inventory rules decide whether the action is legal.

Drag/drop payloads

UMG drag/drop works by creating a drag operation object. The operation can hold a tag, a payload, and a drag visual widget.

For inventory, the payload should answer "what did the user drag?"

Good payload data:

  • source inventory component reference;
  • source slot index;
  • item ID or item view object;
  • quantity being dragged;
  • optional split-stack amount;
  • source panel kind, such as inventory, hotbar, equipment, or shop.

Bad payload data:

  • the only copy of the item count;
  • the old entry widget as the authority;
  • a reference to the collected world pickup actor;
  • a promise that the item has already moved;
  • client-trusted gameplay state in multiplayer.

A beginner-safe operation looks like this:

OnDragDetected from slot/entry widget
-> Create Drag & Drop Operation
   Class = BP_InventoryDragOperation
   Payload = BP_InventoryDragPayload or item view object
   Default Drag Visual = WBP_DragIcon
-> return operation

On the drop target:

OnDrop(Operation)
-> Cast operation payload to BP_InventoryDragPayload
-> Is Valid SourceInventory
-> SourceInventory.RequestMove(SourceSlotIndex, TargetSlotIndex, Quantity)
-> Branch result.bChanged
   true: inventory broadcasts and UI refreshes
   false: show rejected feedback, leave UI state based on inventory

The drag visual is presentation. It is not the item. If the drop is rejected, the item should still be where the inventory owner says it is.

Slot-to-slot moves

Model slot moves as inventory requests:

RequestMove(SourceSlot, TargetSlot, Quantity):
    validate both slots
    validate source has item/count
    if target empty:
        move or split stack
    else if same item and stackable:
        merge up to max stack
    else if swapping allowed:
        swap entries
    else:
        reject
    broadcast once if changed
    return result

The UI should not duplicate that rule by setting two icons and hoping the data matches. If the inventory rejects the move, the next refresh should draw the unchanged state.

This is especially important for equipment slots:

Drop sword onto Weapon slot
-> InventoryComponent.RequestEquip(SourceSlot, Weapon)
-> equip flow validates item type, spawn/attach rules, and old equipped item
-> UI refreshes from the result

The target widget can know that it represents the weapon slot. It should not spawn the weapon actor by itself.

Fixed slots and empty tiles

A fixed grid needs empty slots to exist visually even when no item entry fills them.

Manual grid:

for SlotIndex 0..MaxSlots-1:
    SlotWidget.Setup(SlotIndex, Entry or Empty)

TileView:

Build one item view object per slot:
    SlotIndex 0: Potion x4
    SlotIndex 1: Empty
    SlotIndex 2: RifleAmmo x30

That is different from a compact item list that only shows occupied entries. Pick the model before writing drag/drop. A slot-to-slot inventory needs stable empty positions; a loot list may not.

Refresh lifecycle

Inventory UI usually opens like this:

PlayerController opens inventory
-> Create or show WBP_Inventory
-> WBP_Inventory gets InventoryComponent
-> bind to InventoryComponent.OnInventoryChanged
-> RefreshInventory once

Then runtime changes look like this:

InventoryComponent AddItem/RemoveItem/Move/Equip succeeds
-> broadcast OnInventoryChanged once
-> WBP_Inventory RefreshInventory

If the widget can be removed and reused, unbind/rebind deliberately when the owner changes or the widget is torn down. Dispatchers do not replay old calls, so the initial refresh is still required after binding.

Multiplayer boundary

In multiplayer, inventory UI is local presentation. Inventory mutation is server-authoritative gameplay state.

Client drags item from slot 2 to slot 5
-> UI sends ServerRequestMoveInventoryItem(2, 5)
-> server validates owner, slot, item, distance/rules
-> server mutates inventory
-> replicated inventory summary reaches owning client
-> local widget refreshes

The drag operation can exist only on the local client. That is fine. It is an input gesture. The server should still decide whether the item really moved.

When it fails (and what failure does)

Symptom Likely cause
Rows duplicate every refresh ScrollBox children were not cleared before rebuilding.
Wrong icon appears after scrolling A ListView/TileView entry widget kept old state when reused.
Dragging deletes an item on rejected drop UI removed the item before the inventory accepted the move.
Drop target receives None payload Drag operation class/payload was not assigned or the cast expects the wrong payload type.
Slot clicks affect the wrong item The widget stored a stale slot/index after inventory order changed.
Empty grid slots cannot receive drops The UI only created widgets for occupied entries, not for every fixed slot.
Inventory changes but UI stays stale Widget never bound to OnInventoryChanged, bound to the wrong inventory owner, or skipped initial refresh.
UI works until respawn Widget still points at the old pawn's inventory component. Rebind after possession/respawn.
Multiplayer item moves work only locally Client changed widget/local arrays without a server request and replicated result.

Failure should leave the inventory in its old valid state. Rebuild the UI from inventory state after the result; do not try to repair visuals by guessing.

What this UI does not do

  • It does not own real item counts, durability, or equipped state.
  • It does not make AddItem, RemoveItem, MoveItem, EquipItem, or DropItem rules for you.
  • It does not save inventory to disk.
  • It does not make a ScrollBox virtualized.
  • It does not make ListView entry widgets permanent item objects.
  • It does not replicate client item moves by itself.
  • It does not turn a drag visual into a live item actor.

Lookalikes - which one do I want?

Tool Use when Watch out
Manual slot widgets Fixed bag, hotbar, equipment slots, or first small inventory. You own every create/clear/refresh path.
ScrollBox Small or medium scrollable content where every child can exist. Not virtualized; clear before rebuild.
ListView Large vertical lists or reusable row presentation. Requires item objects and entry widgets that implement the list entry interface.
TileView Virtualized uniform grid/tile inventory. Same item-vs-entry trap as ListView.
UniformGrid / WrapBox Manual grid layout for fixed slots. Layout only; no inventory rules or virtualization.
DragDropOperation A local drag gesture needs payload and drag visual. Payload is a request context, not authority.
Event Dispatcher Inventory changed and widgets/listeners should refresh. Does not replicate or replay old state.

Rule of thumb: start manual for small fixed UI, use ScrollBox for modest lists, graduate to ListView/TileView when virtualization or list behavior is worth the item-object setup.

Going deeper