Start typing to search packages!
popupkit
By @biotoxin495
Roblox
Mirrored from WallyPopupKit — A standalone contextual popup manager
PopupKit is a small, dependency-free Roblox module for contextual UI popups — hover cards, inventory details, pinned item panels, cursor-following tooltips, anchored panels, and directly displayed popups — without depending on a framework or service container.
Popups are registered as named definitions (template or fully custom) and shown either directly or through hover/click triggers. The manager handles placement, boundary clamping and flipping, show/hide delays, pinning, and outside-click dismissal, and returns disposable handles for controlling individual popups and registrations.
Quick example
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PopupKit = require(ReplicatedStorage.PopupKit)
local popups = PopupKit.new({
Parent = screenGui,
Definitions = {
ItemDetails = {
Template = popupTemplates.ItemDetails,
Populate = function(popup, data, context)
popup.Title.Text = data.Name
popup.Description.Text = data.Description
end,
},
},
})
local registration = popups:RegisterTrigger(itemButton, "ItemDetails", function()
return currentItemData
end)
Destroy closes the active popup and tears down every registration and signal owned by the manager.
🚀 Features
- Fully standalone and dependency-free
- One active popup per
PopupKitinstance - Independent manager instances
- Template cloning or fully custom
Createfactories - Static data or dynamic resolver callbacks
- Cursor, top, bottom, left, right, and custom placement
- Start, center, and end alignment
- Boundary clamping and automatic opposite-side flipping
- Configurable show and hide delays
- Interactive hover retention
- Optional click/touch pinning
- Outside-click dismissal
- Active popup update and refresh handles
- Disposable trigger registrations
- Population and creation cleanup callbacks
- Definition lifecycle hooks (
OnOpen,OnClose,OnUpdate,OnPinChanged) - Opened, closed, updated, and pin-state signals
- Global enable, disable, hide, and destruction methods
- Strict-ish Luau types (
--!nonstrict, fully annotated exports)
📖 Basic usage
Copy PopupKit into your project and require it from a client script. PopupKit creates and animates Roblox UI, so it should only run on the client.
Creating a manager
local popups = PopupKit.new({
Parent = screenGui,
Definitions = { --[[ ... ]] },
})
Parent may be:
- A
GuiObject, which PopupKit uses directly as its popup layer - A
LayerCollector, such as aScreenGui, under which PopupKit creates a transparent layer - A
BasePlayerGui, under which PopupKit creates its ownScreenGui nil, in which case PopupKit creates its ownScreenGuiunder the local player'sPlayerGui
Registering a trigger
local registration = popups:RegisterTrigger(
itemButton,
"ItemDetails",
function()
return currentItemData
end,
{
Placement = "Right",
Alignment = "Center",
Offset = Vector2.new(12, 0),
FlipWhenClipped = true,
ShowDelay = 0.08,
HideDelay = 0.1,
KeepOpenWhilePopupHovered = true,
PinOnClick = true,
}
)
The third argument may be static data or a function. Resolver functions receive the trigger and registration as optional arguments.
Popup definitions
A definition must provide exactly one of Template or Create.
-- Template definition
ItemDetails = {
Template = itemDetailsTemplate,
Populate = function(popup, data, context)
popup.Title.Text = data.Name
end,
}
-- Custom creation
Notice = {
Create = function(data, creationContext)
local label = Instance.new("TextLabel")
label.AutomaticSize = Enum.AutomaticSize.XY
label.Text = data.Text
return label, function()
-- Optional creation cleanup, called only when the popup closes.
end
end,
Update = function(popup, data, context)
popup.Text = data.Text
end,
}
Populate runs when a popup is first shown. When its handle is updated, PopupKit uses Update when present; otherwise, it runs Populate again.
Both Populate and Update may return a cleanup function. The previous population cleanup runs before the next update and when the popup closes.
Definition lifecycle hooks
{
OnOpen = function(context) end,
OnClose = function(context, reason) end,
OnUpdate = function(context, oldData, newData) end,
OnPinChanged = function(context, isPinned) end,
}
Hook errors are reported through PopupKit's error handler without invalidating an otherwise usable popup. Errors in Create, Populate, or Update prevent or close the affected popup.
Trigger-relative placement
For Top, Bottom, Left, and Right, PopupKit uses the trigger as the anchor unless another Anchor is supplied.
When FlipWhenClipped is enabled, PopupKit compares the preferred side with its opposite side and uses whichever produces less boundary overflow. The final position is still clamped inside the configured boundary.
Interactive hover behavior
With KeepOpenWhilePopupHovered = true, leaving the trigger does not close the popup while the pointer is over the popup itself. A nonzero HideDelay gives the pointer time to cross the gap between the trigger and popup.
Pinned popups ignore hover-based hide requests until unpinned, replaced with permission, dismissed by an outside click, or explicitly closed.
Direct popups
local handle = popups:Show("Notice", {
Text = "Saved!",
}, {
Placement = "Cursor",
})
For a directly displayed trigger-relative popup, provide Anchor:
popups:Show("Notice", data, {
Anchor = saveButton,
Placement = "Top",
})
Migrating existing populators
Game-specific modules and services should remain outside PopupKit and be captured by your definition factory:
return function(dependencies)
return {
CurrencyPopup = {
Template = dependencies.Templates.CurrencyPopup,
Populate = function(popup, data)
popup.Amount.Text = dependencies.FormatNumber(data.Amount)
end,
},
}
end
PopupKit itself remains framework-independent; consumer definitions may use whatever game-specific dependencies they require.
⚙️ API
Manager
PopupKit.new(config?)
Creates a new PopupKit manager. Each manager owns its own definitions, registrations, and active popup.
local popups = PopupKit.new()
popups:RegisterDefinition(name, definition) / popups:RegisterDefinitions(definitions)
Registers one or several popup definitions. Re-registering a name that has an open popup closes it with reason "DefinitionRemoved".
popups:UnregisterDefinition(name)
Removes a definition, closes its active popup (if any), and destroys every registration using it.
popups:GetDefinition(name)
Returns the stored definition, or nil.
popups:RegisterTrigger(trigger, definitionName, dataSource, options?)
Registers a GuiObject trigger that shows/hides definitionName's popup on hover, returning a registration handle.
popups:Show(definitionName, data, options?)
Shows a popup directly, bypassing triggers, and returns a popup handle.
popups:Hide(reason?)
Closes whichever popup is currently active, regardless of what opened it.
popups:GetActivePopup()
Returns the current popup handle, or nil if nothing is open.
popups:Enable() / popups:Disable() / popups:SetEnabled(enabled) / popups:IsEnabled()
Globally enables or disables the manager. Disabling closes the active popup with reason "Disabled" and prevents new popups from opening until re-enabled.
popups:Destroy()
Closes the active popup, destroys every registration and signal, cancels delayed actions, and removes any GUI layer that PopupKit created itself. Call this when the owning UI/controller is permanently torn down.
Popup handle
PopupKit:Show and Registration:Show return a popup handle.
handle:Update(newData)
handle:Refresh()
handle:Pin()
handle:Unpin()
handle:TogglePinned()
handle:SetPinned(true)
handle:SetOptions({ Placement = "Left" })
handle:Reposition()
handle:Close("Manual")
handle:IsOpen()
handle:IsPinned()
handle:GetInstance()
handle:GetData()
handle:GetOptions()
handle:GetDefinitionName()
handle:GetResolvedPlacement()
handle:GetCloseReason()
Update uses the supplied data. Refresh reruns the registration's data resolver, or reruns the active direct popup with its current data.
Registration
registration:Show()
registration:Show(true) -- Show and pin immediately
registration:Hide()
registration:Refresh()
registration:Pin()
registration:Unpin()
registration:TogglePinned()
registration:SetDataSource(newDataOrResolver)
registration:SetOptions({ Placement = "Left" })
registration:GetTrigger()
registration:GetActivePopup()
registration:IsRegistered()
registration:Destroy()
Destroying a trigger automatically destroys its registration and closes its active popup.
Signals
popups.PopupOpened:Connect(function(handle, context) end)
popups.PopupClosed:Connect(function(handle, reason, context) end)
popups.PopupUpdated:Connect(function(handle, oldData, newData, context) end)
popups.PinnedChanged:Connect(function(handle, isPinned, context) end)
Each signal supports Connect, Once, and Wait.
Complete configuration reference
Manager config (PopupKit.new)
| Property | Type | Default | Description |
|---|---|---|---|
Parent | Instance? | Local player's PlayerGui | Where PopupKit mounts its popup layer. See Creating a manager. |
Boundary | GuiObject? | The popup layer | GuiObject used for clamping and flip calculations. |
Definitions | { [string]: PopupDefinition }? | nil | Definitions registered immediately via RegisterDefinitions. |
DefaultOptions | PopupOptions? | nil | Overrides applied on top of PopupKit's built-in option defaults for every popup. |
ScreenGuiName | string | "PopupKitGui" | Name of the ScreenGui PopupKit creates for itself, if any. |
LayerName | string | "PopupKitLayer" | Name of the transparent Frame layer PopupKit creates under a LayerCollector parent. |
DisplayOrder | number | 100 | DisplayOrder of the ScreenGui PopupKit creates for itself, if any. |
IgnoreGuiInset | boolean | true | IgnoreGuiInset of the ScreenGui PopupKit creates for itself, if any. |
OnError | (stage: string, errorMessage: string) -> ()? | nil | Called alongside PopupKit's internal warn calls whenever a callback errors. |
Popup options
Resolved in this order: PopupKit built-in defaults → DefaultOptions on PopupKit.new → the definition's Options → registration or direct-show options.
| Property | Type | Default | Description |
|---|---|---|---|
Placement | "Cursor" | "Top" | "Bottom" | "Left" | "Right" | "Custom" | "Cursor" | Where the popup appears relative to the cursor or an anchor. |
Alignment | "Start" | "Center" | "End" | "Center" | Cross-axis alignment for Top/Bottom/Left/Right placement. |
Offset | Vector2 | (10, 10) | Offset applied from the cursor or anchor edge. |
EdgePadding | Vector2 | (8, 8) | Minimum padding kept from the boundary's edges. |
FlipWhenClipped | boolean | true | Flips to the opposite side (or axis, for cursor placement) when that reduces boundary overflow. |
TrackPosition | boolean | true | Continuously repositions the popup every frame instead of once on show. |
CustomPosition | (context: PositionContext) -> Vector2? | nil | Required when Placement is "Custom"; returns an absolute screen-space top-left position. |
Anchor | GuiObject? | The trigger, if any | GuiObject used instead of the trigger for relative placement. |
ShowDelay | number | 0.08 | Seconds a trigger must stay hovered before its popup opens. |
HideDelay | number | 0.08 | Seconds after leaving the trigger/popup before it closes. |
KeepOpenWhilePopupHovered | boolean | true | Cancels hide requests while the pointer is over the popup itself. |
PinOnClick | boolean | false | Pins (or toggles pin) on click/touch of the trigger or PinTarget. |
PinTarget | GuiObject? | The trigger | Element whose click/touch pins the popup, when PinOnClick is enabled. |
CloseOnOutsideClick | boolean | true | Closes the popup when clicking/touching outside it, its trigger, and its pin target. |
ReplacePinned | boolean | false | Allows a new popup to replace a currently pinned one. |
RefreshOnShow | boolean | true | Re-resolves and re-populates data when showing a reused popup instance. |
ReuseInstance | boolean | true | Reuses the existing instance instead of recreating it when the same definition/registration is shown again. |
Negative delay values are treated as 0.
📝 Notes
- PopupKit is intended for client-side UI only.
- A popup is automatically closed when its instance is destroyed externally, its trigger or registration is destroyed, its definition is unregistered, or its manager is destroyed.
- All cleanup methods are idempotent, so repeated calls are safe.
🛠️ Installation
Copy PopupKit into your project — for example under ReplicatedStorage:
ReplicatedStorage
└── PopupKit
Then require it from a client script:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PopupKit = require(ReplicatedStorage.PopupKit)
made with ❤️ by biotoxin495
Package Details
Install command (Click to copy)
Version
1.0.4
License
MIT
Safe for commercial use
Automated license review — not legal advice.
