Inventory Dial

Official Inventory Dial documentation for adding a reusable radial quantity selector to Roblox inventory UI, binding controls, configuring stack steps and validating actions.

On this page

Before you install

Inventory Dial is a local UI selector. The dial itself does not require a server script, DataStore, HTTP request or external service.

The supplied package uses two important objects:

  • InventoryDialGui
  • InventoryDial

InventoryDialGui contains the editable dial template. The runtime does not rebuild that UI. It clones the supplied template whenever the player opens the selector.

The original template should remain at:

StarterGui
└── InventoryDialGui
    └── dial
        └── dial

The runtime expects the inner dial object to be a Frame.

For actual inventory consumption, item removal or currency changes, the server should still validate the requested amount.

Installation

Move InventoryDialGui into StarterGui.

Move the InventoryDial LocalScript into StarterPlayer > StarterPlayerScripts.

Your Explorer should look like this:

StarterGui
└── InventoryDialGui
    └── dial
        └── dial
            ├── Value
            └── your radial progress objects

StarterPlayer
└── StarterPlayerScripts
    └── InventoryDial

The final runtime paths are:

game.Players.LocalPlayer.PlayerGui.InventoryDialGui.dial.dial

and:

game.Players.LocalPlayer.PlayerScripts.InventoryDial

No additional runtime UI generator is required.

Quick start

The easiest way to use Inventory Dial is Bind.

local Dial = shared.InventoryDial

Dial.Bind(
    ItemButton,

    function()
        return ItemAmount
    end,

    function(amount)
        print("Selected:", amount)
    end
)

ItemButton can be any GuiObject.

The amount function is called when the player begins using the dial, so it can return the current live stack size.

For an inventory item:

Dial.Bind(
    ItemButton,

    function()
        return Item.Amount
    end,

    function(amount)
        UseItemRemote:FireServer(
            Item.Id,
            amount
        )
    end
)

How it works

When the player begins using a bound UI object:

  1. The runtime reads the current available amount.
  2. The supplied InventoryDialGui > dial > dial template is cloned.
  3. The clone is placed directly over the selected UI object.
  4. Pointer movement around the center is converted into a percentage from 0 to 1.
  5. The percentage is converted into a valid amount.
  6. The amount is snapped to the current stack step.
  7. The radial progress follows the pointer smoothly.
  8. The center amount text eases toward the selected value instead of instantly jumping through large numbers.
  9. Releasing confirms the selected amount.
  10. The cloned dial is destroyed while the original template remains untouched.

The selected amount and the displayed amount are intentionally separate.

The selected amount always remains a valid stepped value. The displayed text smoothly catches up to that value so large stack changes are easier to read.

API

Bind

Dial.Bind(
    target,
    getAmount,
    onConfirm,
    options
)

Binds the dial to a GuiObject.

getAmount can be either:

  • A number
  • A function that returns a number

Using a function is recommended for inventory systems because it reads the latest stack amount whenever the dial opens.

Example:

Dial.Bind(
    Slot,

    function()
        return CurrentAmount
    end,

    function(amount)
        print(amount)
    end
)

Unbind

Dial.Unbind(Slot)

Removes a previous binding from the supplied UI object.

Begin

Dial.Begin(
    target,
    maximum,
    input,
    options
)

Starts an interactive drag manually.

Use this when another LocalScript already handles InputBegan.

Example:

Slot.InputBegan:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.MouseButton1 then
        Dial.Begin(
            Slot,
            CurrentAmount,
            input,
            {
                OnConfirm = function(amount)
                    print(amount)
                end,
            }
        )
    end
end)

Show

Dial.Show(
    target,
    maximum,
    options
)

Shows the dial programmatically without automatically starting mouse dragging.

This is useful when another UI system controls the dial through API calls.

Example:

Dial.Show(
    ItemFrame,
    500,
    {
        StartValue = 100,
        OnConfirm = function(amount)
            print(amount)
        end,
    }
)

SetValue

Dial.SetValue(250)

Changes the current selected value while the dial is open.

The value is clamped and snapped using the current step.

GetValue

local amount = Dial.GetValue()

Returns the current selected amount.

GetDisplayValue

local amount = Dial.GetDisplayValue()

Returns the current smoothed number being displayed in the center.

GetMaximum

local maximum = Dial.GetMaximum()

Returns the current maximum selectable amount.

GetStep

local step = Dial.GetStep()

Returns the current selection step.

Confirm

Dial.Confirm()

Confirms the current selected value and runs OnConfirm.

Cancel

Dial.Cancel("ClosedByInventory")

Closes the dial without confirming the selected amount.

If OnCancel is supplied, the reason is passed to the callback.

Close

Dial.Close()

Immediately closes and removes the active cloned dial.

No confirmation or cancel callback is fired.

IsOpen

if Dial.IsOpen() then
    print("Dial open")
end

GetTemplate

local template = Dial.GetTemplate()

Returns the original InventoryDialGui > dial > dial template.

GetActive

local activeDial = Dial.GetActive()

Returns the current runtime clone or nil.

Options

Most API calls accept an options table.

Example:

{
    CenterOn = ItemIcon,
    Step = 10,
    StartValue = 0,
    TextSmoothSpeed = 10,
    ProgressSmoothSpeed = 28,
    Debug = false,

    OnOpen = function(dial)
    end,

    OnChanged = function(value, maximum, step)
    end,

    OnConfirm = function(value)
    end,

    OnCancel = function(reason)
    end,
}

CenterOn

CenterOn = ItemIcon

By default, the dial is centered on the same GuiObject passed as the target.

Use CenterOn when the clickable object and the visual inventory item are different objects.

Example:

ItemButton
└── ItemIcon
Dial.Bind(
    ItemButton,
    getAmount,
    onConfirm,
    {
        CenterOn = ItemIcon,
    }
)

Step

Overrides automatic amount stepping.

Step = 50

StartValue

Controls the starting selected amount when the dial opens.

StartValue = 25

TextSmoothSpeed

Controls how quickly the center number visually catches up to the selected value.

TextSmoothSpeed = 10

Lower values make the text easier to follow but slower to catch up.

Higher values react faster.

The actual selected value is not delayed.

ProgressSmoothSpeed

Controls how quickly the radial progress catches up to pointer movement.

ProgressSmoothSpeed = 28

Amount stepping

Inventory Dial automatically changes selection sensitivity based on the available stack amount.

The default rules are:

Maximum stack        Selection step

1 - 100              1
101 - 2,500          5
2,501 - 10,000       25
10,001 - 50,000      100
50,001 - 250,000     500
250,001 - 1,000,000  2,500
1,000,001+           5,000

The exact maximum stack amount is always selectable even when the maximum is not evenly divisible by the current step.

For example, a maximum of 2,503 can still select exactly 2,503.

Override the automatic step for a specific binding with:

{
    Step = 1,
}

Template customization

The original dial template is located at:

StarterGui
└── InventoryDialGui
    └── dial
        └── dial

Edit this template directly in Studio.

The runtime does not change the template’s:

  • Size
  • AnchorPoint
  • TextSize
  • TextScaled
  • Font
  • Colors
  • Decorative UI
  • Layout

The Value TextLabel should use:

TextScaled = false

Choose the exact TextSize you want in Studio.

The runtime only changes:

Value.Text

on the cloned dial.

For predictable sizing across different UI parents, an offset-based dial size is recommended.

Example:

Size = UDim2.fromOffset(57, 57)

Radial progress

The runtime supports a gradient progress setup when the template contains:

RadialProgress
├── Left
│   └── Fill
│       └── Gradient
└── Right
    └── Fill
        └── Gradient

It also supports older segment-based templates as a fallback.

The gradient version is recommended because it avoids updating a large number of individual frames.

Debug mode

Debug mode is disabled by default.

Enable it globally:

Dial.SetDebug(true)

Disable it:

Dial.SetDebug(false)

You can also enable debugging for one open call:

Dial.Show(
    Slot,
    500,
    {
        Debug = true,
    }
)

Debug mode prints useful information including:

  • Template path
  • Template size
  • Active clone path
  • Target path
  • Center target path
  • Target absolute position and size
  • Dial absolute position and size
  • Maximum amount
  • Selection step
  • Selected amount
  • Displayed amount
  • Progress implementation
  • Open and close reasons

For a full state report:

Dial.DebugDump()

If positioning looks incorrect, run DebugDump() while the dial is visible and compare the target center with the active dial center.

Server validation

Inventory Dial is a local selector.

Do not treat the amount returned by the client as trusted inventory state.

For a real consumable system:

Dial.Bind(
    ItemButton,

    function()
        return ClientDisplayedAmount
    end,

    function(amount)
        ConsumeRemote:FireServer(
            ItemId,
            amount
        )
    end
)

The server should independently confirm:

  • The item exists.
  • The player owns the item.
  • The player owns at least the requested amount.
  • The amount is valid for that item.
  • The action is currently allowed.

The server should then perform the actual inventory change.

Troubleshooting

The dial does not appear

Confirm the template exists at:

StarterGui
└── InventoryDialGui
    └── dial
        └── dial

Also confirm InventoryDial is a LocalScript inside:

StarterPlayer
└── StarterPlayerScripts
    └── InventoryDial

Enable debug mode:

shared.InventoryDial.SetDebug(true)

The dial is not centered

By default, Inventory Dial centers on the exact GuiObject passed to Bind, Begin or Show.

If the clickable button contains a smaller visual item, explicitly set:

{
    CenterOn = ItemIcon,
}

Then run:

Dial.DebugDump()

The debug output prints the target and dial centers.

The dial size changes

The runtime never assigns a new Size to the cloned dial.

If the template uses Scale values, reparenting it under a differently sized UI object can change its absolute pixel size.

For a fixed visual size, use Offset sizing on the template:

Size = UDim2.fromOffset(57, 57)

The number changes too quickly

Lower:

TextSmoothSpeed

Example:

{
    TextSmoothSpeed = 7,
}

The selected amount remains responsive while the visible text catches up more slowly.

The number is changing size

Confirm the Value TextLabel has:

TextScaled = false

and remove any UITextSizeConstraint if you do not want automatic text resizing.

Inventory Dial does not change either property.

The progress does not move

If using the gradient implementation, confirm the template contains both UIGradient objects under RadialProgress.

If using an older segmented template, confirm the progress frames contain Segment in their names.

Enable debug mode to see which progress implementation was detected.

The dial works but consuming items is insecure

The UI is intentionally local.

The server must validate and apply real inventory changes. Never directly trust a client-selected amount.