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:
InventoryDialGuiInventoryDial
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:
- The runtime reads the current available amount.
- The supplied
InventoryDialGui > dial > dialtemplate is cloned. - The clone is placed directly over the selected UI object.
- Pointer movement around the center is converted into a percentage from
0to1. - The percentage is converted into a valid amount.
- The amount is snapped to the current stack step.
- The radial progress follows the pointer smoothly.
- The center amount text eases toward the selected value instead of instantly jumping through large numbers.
- Releasing confirms the selected amount.
- 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:
SizeAnchorPointTextSizeTextScaled- 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.
