Forest Logo
search
package_2

popupkit

By @biotoxin495

Roblox

Mirrored from Wally

PopupKit — 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 PopupKit instance
  • Independent manager instances
  • Template cloning or fully custom Create factories
  • 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 a ScreenGui, under which PopupKit creates a transparent layer
  • A BasePlayerGui, under which PopupKit creates its own ScreenGui
  • nil, in which case PopupKit creates its own ScreenGui under the local player's PlayerGui

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)

PropertyTypeDefaultDescription
ParentInstance?Local player's PlayerGuiWhere PopupKit mounts its popup layer. See Creating a manager.
BoundaryGuiObject?The popup layerGuiObject used for clamping and flip calculations.
Definitions{ [string]: PopupDefinition }?nilDefinitions registered immediately via RegisterDefinitions.
DefaultOptionsPopupOptions?nilOverrides applied on top of PopupKit's built-in option defaults for every popup.
ScreenGuiNamestring"PopupKitGui"Name of the ScreenGui PopupKit creates for itself, if any.
LayerNamestring"PopupKitLayer"Name of the transparent Frame layer PopupKit creates under a LayerCollector parent.
DisplayOrdernumber100DisplayOrder of the ScreenGui PopupKit creates for itself, if any.
IgnoreGuiInsetbooleantrueIgnoreGuiInset of the ScreenGui PopupKit creates for itself, if any.
OnError(stage: string, errorMessage: string) -> ()?nilCalled 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.

PropertyTypeDefaultDescription
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.
OffsetVector2(10, 10)Offset applied from the cursor or anchor edge.
EdgePaddingVector2(8, 8)Minimum padding kept from the boundary's edges.
FlipWhenClippedbooleantrueFlips to the opposite side (or axis, for cursor placement) when that reduces boundary overflow.
TrackPositionbooleantrueContinuously repositions the popup every frame instead of once on show.
CustomPosition(context: PositionContext) -> Vector2?nilRequired when Placement is "Custom"; returns an absolute screen-space top-left position.
AnchorGuiObject?The trigger, if anyGuiObject used instead of the trigger for relative placement.
ShowDelaynumber0.08Seconds a trigger must stay hovered before its popup opens.
HideDelaynumber0.08Seconds after leaving the trigger/popup before it closes.
KeepOpenWhilePopupHoveredbooleantrueCancels hide requests while the pointer is over the popup itself.
PinOnClickbooleanfalsePins (or toggles pin) on click/touch of the trigger or PinTarget.
PinTargetGuiObject?The triggerElement whose click/touch pins the popup, when PinOnClick is enabled.
CloseOnOutsideClickbooleantrueCloses the popup when clicking/touching outside it, its trigger, and its pin target.
ReplacePinnedbooleanfalseAllows a new popup to replace a currently pinned one.
RefreshOnShowbooleantrueRe-resolves and re-populates data when showing a reused popup instance.
ReuseInstancebooleantrueReuses 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

check_circle

Safe for commercial use

Automated license review — not legal advice.