{"id":"biotoxin495/dropdown","name":"dropdown","scope":"biotoxin495","platform":"roblox","description":"An efficient dependency-free dropdown UI creator and controller for Roblox.","version":"1.0.1","latest":"1.0.1","versions":["1.0.0","1.0.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"sleitnick/signal":{"version":"^2.0.3","alias":"Signal"}},"integrity":"3d1c962694939b40f463afe75bc64d1241d8495365937dd972185d382bf34ac2","likes":0,"downloads":0,"install":"forest install biotoxin495/dropdown","url":"https://forest.dev/p/roblox/biotoxin495/dropdown","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/dropdown/files","readme":"# Dropdown — Dependency-free dropdown UI controller for Roblox\n\n**Dropdown**, a flexible, dependency-free dropdown UI controller for Roblox.\n\nTurning a `GuiButton` into a dropdown usually means manually building option rendering, selection state, positioning, outside-click detection, and cleanup logic by hand.\n\n**Dropdown** handles this for you. It can generate its own dropdown interface automatically, or it can control completely custom Studio-authored dropdowns and entry templates.\n\n## Quick example\n\n```lua\nlocal Dropdown = require(ReplicatedStorage:WaitForChild(\"Dropdown\"))\n\nlocal dropdown = Dropdown.new(script.Parent.SortButton, {\n\tOptions = {\n\t\t{ Id = \"rarity\", Text = \"Rarity\" },\n\t\t{ Id = \"newest\", Text = \"Newest\" },\n\t\t{ Id = \"game\", Text = \"Game\" },\n\t},\n})\n\ndropdown.Selected:Connect(function(option)\n\tprint(\"Selected:\", option.Id)\nend)\n```\n\nThe module automatically connects to the trigger button, creates the dropdown and its entries, positions the dropdown, handles selection, closes on outside clicks, and cleans up its connections when destroyed.\n\n## 🚀 Features\n\n- Attach a dropdown to any `GuiButton`\n- Automatically generated dropdown UI\n- Custom dropdown instances and templates\n- Custom entry templates\n- Custom entry renderers\n- Single-selection dropdowns\n- Multiple-selection dropdowns\n- Action menus with no managed selection\n- Dynamic option datasets\n- Disabled options\n- Automatic dropdown positioning\n- Bottom, top, left, right, and automatic placement\n- Automatic screen-edge flipping\n- Screen-bound clamping\n- Automatic sizing and scrolling\n- Outside-click closing\n- Escape-to-close support\n- Exclusive dropdown behavior\n- Optional trigger text synchronization\n- Overlay rendering to avoid `ClipsDescendants`\n- Lightweight open/close scale animation\n- Per-dropdown state and connection management\n- Complete lifecycle cleanup\n- No external dependencies\n\n## 🛠️ Installation\n\nPlace the `Dropdown` ModuleScript somewhere that client code can access it.\n\nA common setup is:\n\n```text\nReplicatedStorage\n└── Dropdown\n```\n\nThen require it from a LocalScript:\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Dropdown = require(ReplicatedStorage:WaitForChild(\"Dropdown\"))\n```\n\n`Dropdown` is intended for client-side UI code.\n\n## 📖 Basic Usage\n\nCreate a dropdown by supplying a trigger button and a configuration table.\n\n```lua\nlocal dropdown = Dropdown.new(triggerButton, config)\n```\n\n`triggerButton` must be a `GuiButton`, such as a `TextButton` or `ImageButton`.\n\nBy default, pressing the trigger toggles the dropdown open and closed. Disable that with `ToggleOnTrigger = false` if you’d rather drive `Open`/`Close`/`Toggle` yourself.\n\nWhen the dropdown is no longer needed, call `Destroy`:\n\n```lua\ndropdown:Destroy()\n```\n\n## Options\n\nDropdown options are data-driven.\n\nA normal option can look like:\n\n```lua\n{\n\tId = \"rare\",\n\tText = \"Rare\",\n\tValue = 4,\n\tDisabled = false,\n}\n```\n\nOnly `Id` is important to the dropdown’s internal selection system.\n\n### `Id`\n\n```lua\nId = \"rare\"\n```\n\nUnique identifier for the option. IDs should be unique within a dropdown. If omitted, the option’s array index is used.\n\n### `Text`\n\n```lua\nText = \"Rare\"\n```\n\nText displayed by the default renderer. If omitted, the module attempts to derive text from `Name`, `Value`, or `Id`.\n\n### `Value`\n\n```lua\nValue = 4\n```\n\nOptional application-specific value associated with the option. This can be anything your application needs — the dropdown uses `Id` for selection state while your game can use `Value`.\n\n### `Disabled`\n\n```lua\nDisabled = true\n```\n\nPrevents the option from being selected. Disabled generated entries also use the configured disabled visual style.\n\n### Simple Values\n\nOptions do not have to be full tables.\n\n```lua\nOptions = {\n\t\"Rarity\",\n\t\"Newest\",\n\t\"Game\",\n}\n```\n\nThe module automatically normalizes these into option objects. For more advanced dropdowns, explicit option tables are recommended.\n\n## Selection Modes\n\nThere are three selection modes.\n\n### Single Selection\n\nThe default mode.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tSelectionMode = \"Single\",\n\n\tOptions = {\n\t\t{ Id = \"rarity\", Text = \"Rarity\" },\n\t\t{ Id = \"newest\", Text = \"Newest\" },\n\t\t{ Id = \"game\", Text = \"Game\" },\n\t},\n})\n```\n\nOnly one option can be selected at a time. By default, the dropdown closes after a selection.\n\n### Multiple Selection\n\nUseful for filters and checklists.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tSelectionMode = \"Multiple\",\n\tCloseOnSelect = false,\n\n\tOptions = {\n\t\t{ Id = \"common\", Text = \"Common\" },\n\t\t{ Id = \"rare\", Text = \"Rare\" },\n\t\t{ Id = \"epic\", Text = \"Epic\" },\n\t},\n})\n```\n\nRetrieve selected IDs with `dropdown:GetSelected()`, or full option objects with `dropdown:GetSelectedOptions()`.\n\n### No Selection\n\nUse `\"None\"` when the dropdown represents actions rather than persistent choices.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tSelectionMode = \"None\",\n\n\tOptions = {\n\t\t{ Id = \"duplicate\", Text = \"Duplicate\" },\n\t\t{ Id = \"rename\", Text = \"Rename\" },\n\t\t{ Id = \"delete\", Text = \"Delete\" },\n\t},\n})\n\ndropdown.Selected:Connect(function(option)\n\tprint(\"Action:\", option.Id)\nend)\n```\n\nThis is useful for context menus, action menus, overflow menus, and command lists.\n\n## Default Selection\n\nFor single-selection dropdowns:\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tSelected = \"newest\",\n\n\tOptions = {\n\t\t{ Id = \"rarity\", Text = \"Rarity\" },\n\t\t{ Id = \"newest\", Text = \"Newest\" },\n\t},\n})\n```\n\nFor multiple-selection dropdowns:\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tSelectionMode = \"Multiple\",\n\tSelected = { \"common\", \"rare\" },\n\n\tOptions = {\n\t\t{ Id = \"common\", Text = \"Common\" },\n\t\t{ Id = \"rare\", Text = \"Rare\" },\n\t\t{ Id = \"epic\", Text = \"Epic\" },\n\t},\n})\n```\n\n## Dynamic Options\n\nOptions can be replaced at runtime.\n\n```lua\ndropdown:SetOptions({\n\t{ Id = \"one\", Text = \"One\" },\n\t{ Id = \"two\", Text = \"Two\" },\n\t{ Id = \"three\", Text = \"Three\" },\n})\n```\n\nThe dropdown automatically rebuilds its entries and drops any selection values that no longer exist. Additional helpers are available:\n\n```lua\ndropdown:AddOption({ Id = \"four\", Text = \"Four\" })\ndropdown:RemoveOption(\"two\")\ndropdown:ClearOptions()\n```\n\n## Reading Selection\n\n### Single Selection\n\n```lua\nlocal selectedId = dropdown:GetSelected()\n\nlocal option = dropdown:GetSelectedOption()\nif option then\n\tprint(option.Text)\n\tprint(option.Value)\nend\n```\n\n### Multiple Selection\n\n```lua\nlocal selectedIds = dropdown:GetSelected()\nlocal selectedOptions = dropdown:GetSelectedOptions()\n```\n\n## Changing Selection\n\n```lua\ndropdown:SetSelected(\"rare\")\n```\n\nFor multiple-selection dropdowns:\n\n```lua\ndropdown:SetSelected(\"rare\", true)\ndropdown:SetSelected(\"epic\", true)\ndropdown:SetSelected(\"common\", false)\n```\n\nClear all selection with `dropdown:ClearSelection()`.\n\n## Opening and Closing\n\n```lua\ndropdown:Open()\ndropdown:Close()\ndropdown:Toggle()\n\nif dropdown:IsOpen() then\n\tprint(\"Dropdown is open\")\nend\n```\n\n## Trigger Behavior\n\nBy default, pressing the supplied trigger button toggles the dropdown. Disable that behavior with:\n\n```lua\nToggleOnTrigger = false\n```\n\nYou can then control the dropdown manually:\n\n```lua\nButton.Activated:Connect(function()\n\tdropdown:Open()\nend)\n```\n\n## Updating Trigger Text\n\nGenerated dropdowns do not modify the trigger’s text unless requested.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tUpdateTriggerText = true,\n\n\tOptions = {\n\t\t{ Id = \"rarity\", Text = \"Rarity\" },\n\t\t{ Id = \"newest\", Text = \"Newest\" },\n\t},\n})\n```\n\nWith single selection, the selected option text becomes the button text. With multiple selection, the trigger displays the number of selected options. For full control, use `RenderTrigger`.\n\n## Dropdown Placement\n\nThe dropdown can automatically position itself relative to its trigger.\n\n```lua\nPlacement = \"Auto\"\n```\n\nSupported values: `Auto`, `Bottom`, `Top`, `Left`, `Right`.\n\n`Auto` prefers placing the dropdown beneath the trigger and flips it upward when there is not enough room.\n\n### Alignment\n\n```lua\nAlignment = \"Start\"\n```\n\nSupported values: `Start`, `Center`, `End`.\n\n### Offset\n\n```lua\nOffset = Vector2.new(0, 6)\n```\n\nThis adds spacing between the trigger and dropdown.\n\n### Automatic Flipping\n\nEnabled by default:\n\n```lua\nAutoFlip = true\n```\n\nFor example, a dropdown configured for `\"Bottom\"` can automatically open above its trigger if there is not enough room beneath it.\n\n### Screen Clamping\n\nEnabled by default:\n\n```lua\nClampToScreen = true\nScreenPadding = 8\n```\n\nThis prevents the dropdown from extending beyond the visible UI area.\n\n## Automatic Sizing\n\nGenerated dropdowns size themselves from their option count.\n\n```lua\nWidth = 220\nMinWidth = 160\nMaxHeight = 280\n\nEntryHeight = 38\nEntryPadding = 4\nContentPadding = 6\n```\n\nWhen the option list becomes taller than `MaxHeight`, the generated dropdown becomes scrollable. If `Width` is omitted, the dropdown is at least as wide as its trigger.\n\n## Open/Close Animation\n\nGenerated dropdowns animate open and closed with a `UIScale` tween by default.\n\n```lua\nAnimation = {\n\tEnabled = true,\n\tDuration = 0.12,\n\tClosedScale = 0.96,\n\tEasingStyle = Enum.EasingStyle.Quad,\n\tEasingDirection = Enum.EasingDirection.Out,\n}\n```\n\nSet `Animation.Enabled = false` for the dropdown to appear and disappear instantly. When using an existing `DropdownInstance` or `DropdownTemplate` without a `UIScale`, the module creates one automatically so the animation still has something to drive.\n\n## Styling Generated UI\n\nThe generated dropdown exposes property tables for its main visual states.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tStyle = {\n\t\tContainer = {\n\t\t\tBackgroundColor3 = Color3.fromRGB(20, 22, 26),\n\t\t},\n\n\t\tEntry = {\n\t\t\tBackgroundColor3 = Color3.fromRGB(35, 38, 44),\n\t\t\tTextColor3 = Color3.fromRGB(240, 240, 240),\n\t\t\tTextSize = 16,\n\t\t},\n\n\t\tEntrySelected = {\n\t\t\tBackgroundColor3 = Color3.fromRGB(60, 110, 230),\n\t\t},\n\n\t\tEntryDisabled = {\n\t\t\tBackgroundTransparency = 0.35,\n\t\t\tTextTransparency = 0.45,\n\t\t},\n\t},\n})\n```\n\nThese tables are applied directly to the relevant generated Roblox Instances. Invalid properties produce a warning rather than breaking the dropdown.\n\n## Custom Entry Templates\n\nYou can provide your own entry template.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tEntryTemplate = EntryTemplate,\n\tOptions = options,\n})\n```\n\nThe template is cloned once per option. If the entry itself is not a `GuiButton`, the module searches its descendants for one. For explicit control:\n\n```lua\nGetEntryButton = function(entry, option)\n\treturn entry.Checkbox\nend\n```\n\n## Custom Entry Rendering\n\nFor complete visual control, provide `RenderOption`.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tEntryTemplate = EntryTemplate,\n\n\tGetEntryButton = function(entry)\n\t\treturn entry.Checkbox\n\tend,\n\n\tRenderOption = function(entry, option, state)\n\t\tentry.TextLabel.Text = option.Text\n\t\tentry.SelectedIndicator.Visible = state.Selected\n\tend,\n\n\tOptions = options,\n})\n```\n\nThe renderer receives `state.Selected`, `state.Disabled`, and `state.Index`. The module owns state and interactions while your renderer owns appearance.\n\n## Custom Entry Creation\n\nInstead of cloning `EntryTemplate`, entries can be created dynamically.\n\n```lua\nCreateEntry = function(option, index, dropdown)\n\tlocal button = Instance.new(\"TextButton\")\n\tbutton.Size = UDim2.new(1, 0, 0, 40)\n\tbutton.Text = option.Text\n\treturn button\nend\n```\n\nThe returned object must be a `GuiObject`.\n\n## Custom Dropdown Templates\n\nThe entire dropdown container can also be replaced.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tDropdownTemplate = MyDropdownTemplate,\n\tEntryTemplate = MyEntryTemplate,\n\tOptions = options,\n})\n```\n\nThe dropdown template is cloned for this controller. The module still manages visibility, positioning, outside-click detection, selection, option rendering, lifecycle, and exclusivity.\n\n### Entry Container\n\nWhen using a custom dropdown template, tell the module where option entries should be parented:\n\n```lua\nEntryContainer = \"Entries\"\n```\n\nThe name is searched recursively inside the dropdown. You can also provide the Instance directly, or use a resolver:\n\n```lua\nGetEntryContainer = function(dropdown)\n\treturn dropdown.MainCategoryHolder.Scroll\nend\n```\n\nIf nothing is specified, the module attempts to find `Entries`, `Scroll`, or `Content`, and then falls back to the first `ScrollingFrame`.\n\n## Existing Dropdown Instances\n\nInstead of cloning a template, the module can control an existing UI object.\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tDropdownInstance = ExistingDropdown,\n\tEntryContainer = ExistingDropdown.Scroll,\n\tEntryTemplate = EntryTemplate,\n\tOptions = options,\n})\n```\n\nThe caller retains ownership of `DropdownInstance`. Calling `dropdown:Destroy()` does not destroy the supplied dropdown instance, and its original visibility state is restored.\n\n## Overlay / Portal Rendering\n\nGenerated dropdowns are automatically placed inside a transparent overlay under the trigger’s `ScreenGui`. This prevents common clipping problems caused by a `ClipsDescendants` ancestor. You can override its parent with:\n\n```lua\nParent = MyOverlayFrame\n```\n\n## Exclusive Dropdowns\n\nBy default:\n\n```lua\nExclusive = true\n```\n\nWhen one exclusive dropdown opens, another currently open exclusive dropdown closes. This prevents multiple normal dropdown menus from stacking on top of one another. Disable this behavior when needed with `Exclusive = false`.\n\n## Closing Behavior\n\n```lua\nCloseOnSelect = true\nCloseOnOutsideClick = true\nCloseOnEscape = true\n```\n\nIf `CloseOnSelect` is omitted, `\"Single\"` closes after selection, `\"None\"` closes after activation, and `\"Multiple\"` stays open.\n\n## Enabling / Disabling\n\n```lua\ndropdown:SetEnabled(false)\n```\n\nA disabled dropdown cannot be opened or interacted with, and closes immediately if it was open. Check the state with `dropdown:IsEnabled()`.\n\n## Signals\n\nEvery controller exposes four signals.\n\n### `Opened`\n\n```lua\ndropdown.Opened:Connect(function()\n\tprint(\"Opened\")\nend)\n```\n\n### `Closed`\n\n```lua\ndropdown.Closed:Connect(function()\n\tprint(\"Closed\")\nend)\n```\n\n### `Selected`\n\nFires whenever an enabled option is activated.\n\n```lua\ndropdown.Selected:Connect(function(option, index)\n\tprint(option.Id, index)\nend)\n```\n\nThis signal also works when `SelectionMode = \"None\"`, making it useful for action menus.\n\n### `SelectionChanged`\n\nFires whenever managed selection changes.\n\n```lua\ndropdown.SelectionChanged:Connect(function(selected, selectedOptions)\n\tprint(selected)\nend)\n```\n\nFor single-selection dropdowns, `selected` is the selected ID. For multiple-selection dropdowns, `selected` is an array of selected IDs. `selectedOptions` is always an array containing the selected option objects.\n\n## ⚙️ API Reference\n\n### `Dropdown.new`\n\n```lua\nDropdown.new(triggerButton: GuiButton, config: table?)\n```\n\nCreates a dropdown controller.\n\n### Visibility\n\n```lua\ndropdown:Open()\ndropdown:Close()\ndropdown:Toggle()\ndropdown:IsOpen()\n```\n\n### Enabled State\n\n```lua\ndropdown:SetEnabled(enabled)\ndropdown:IsEnabled()\n```\n\n### Options\n\n```lua\ndropdown:SetOptions(options)\ndropdown:GetOptions()\ndropdown:GetOption(id)\n\ndropdown:AddOption(option)\ndropdown:RemoveOption(id)\ndropdown:ClearOptions()\n```\n\n### Selection\n\n```lua\ndropdown:SetSelected(id, selected)\ndropdown:GetSelected()\n\ndropdown:GetSelectedOption()\ndropdown:GetSelectedOptions()\n\ndropdown:ClearSelection()\n```\n\n### `Refresh`\n\n```lua\ndropdown:Refresh()\n```\n\nRe-renders all entries, updates generated sizing, refreshes positioning when open, and refreshes the trigger renderer. Useful when external data used by a custom renderer changes.\n\n### `Destroy`\n\n```lua\ndropdown:Destroy()\n```\n\nDisconnects all controller connections and destroys module-owned UI and signals. Always call `Destroy()` when a dropdown controller is permanently no longer needed.\n\n## Configuration Reference\n\nA representative configuration looks like:\n\n```lua\nlocal dropdown = Dropdown.new(Button, {\n\tOptions = {},\n\n\tSelectionMode = \"Single\",\n\tSelected = nil,\n\n\tEnabled = true,\n\tToggleOnTrigger ","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/biotoxin495/roblox/dropdown/1.0.1/readme"}