Forest Logo
search
package_2

dropdown

By @biotoxin495

Roblox

Mirrored

Dropdown — Dependency-free dropdown UI controller for Roblox

Dropdown, a flexible, dependency-free dropdown UI controller for Roblox.

Turning a GuiButton into a dropdown usually means manually building option rendering, selection state, positioning, outside-click detection, and cleanup logic by hand.

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.

Quick example

local Dropdown = require(ReplicatedStorage:WaitForChild("Dropdown"))

local dropdown = Dropdown.new(script.Parent.SortButton, {
	Options = {
		{ Id = "rarity", Text = "Rarity" },
		{ Id = "newest", Text = "Newest" },
		{ Id = "game", Text = "Game" },
	},
})

dropdown.Selected:Connect(function(option)
	print("Selected:", option.Id)
end)

The 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.

🚀 Features

  • Attach a dropdown to any GuiButton
  • Automatically generated dropdown UI
  • Custom dropdown instances and templates
  • Custom entry templates
  • Custom entry renderers
  • Single-selection dropdowns
  • Multiple-selection dropdowns
  • Action menus with no managed selection
  • Dynamic option datasets
  • Disabled options
  • Automatic dropdown positioning
  • Bottom, top, left, right, and automatic placement
  • Automatic screen-edge flipping
  • Screen-bound clamping
  • Automatic sizing and scrolling
  • Outside-click closing
  • Escape-to-close support
  • Exclusive dropdown behavior
  • Optional trigger text synchronization
  • Overlay rendering to avoid ClipsDescendants
  • Lightweight open/close scale animation
  • Per-dropdown state and connection management
  • Complete lifecycle cleanup
  • No external dependencies

🛠️ Installation

Place the Dropdown ModuleScript somewhere that client code can access it.

A common setup is:

ReplicatedStorage
└── Dropdown

Then require it from a LocalScript:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Dropdown = require(ReplicatedStorage:WaitForChild("Dropdown"))

Dropdown is intended for client-side UI code.

📖 Basic Usage

Create a dropdown by supplying a trigger button and a configuration table.

local dropdown = Dropdown.new(triggerButton, config)

triggerButton must be a GuiButton, such as a TextButton or ImageButton.

By default, pressing the trigger toggles the dropdown open and closed. Disable that with ToggleOnTrigger = false if you’d rather drive Open/Close/Toggle yourself.

When the dropdown is no longer needed, call Destroy:

dropdown:Destroy()

Options

Dropdown options are data-driven.

A normal option can look like:

{
	Id = "rare",
	Text = "Rare",
	Value = 4,
	Disabled = false,
}

Only Id is important to the dropdown’s internal selection system.

Id

Id = "rare"

Unique identifier for the option. IDs should be unique within a dropdown. If omitted, the option’s array index is used.

Text

Text = "Rare"

Text displayed by the default renderer. If omitted, the module attempts to derive text from Name, Value, or Id.

Value

Value = 4

Optional 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.

Disabled

Disabled = true

Prevents the option from being selected. Disabled generated entries also use the configured disabled visual style.

Simple Values

Options do not have to be full tables.

Options = {
	"Rarity",
	"Newest",
	"Game",
}

The module automatically normalizes these into option objects. For more advanced dropdowns, explicit option tables are recommended.

Selection Modes

There are three selection modes.

Single Selection

The default mode.

local dropdown = Dropdown.new(Button, {
	SelectionMode = "Single",

	Options = {
		{ Id = "rarity", Text = "Rarity" },
		{ Id = "newest", Text = "Newest" },
		{ Id = "game", Text = "Game" },
	},
})

Only one option can be selected at a time. By default, the dropdown closes after a selection.

Multiple Selection

Useful for filters and checklists.

local dropdown = Dropdown.new(Button, {
	SelectionMode = "Multiple",
	CloseOnSelect = false,

	Options = {
		{ Id = "common", Text = "Common" },
		{ Id = "rare", Text = "Rare" },
		{ Id = "epic", Text = "Epic" },
	},
})

Retrieve selected IDs with dropdown:GetSelected(), or full option objects with dropdown:GetSelectedOptions().

No Selection

Use "None" when the dropdown represents actions rather than persistent choices.

local dropdown = Dropdown.new(Button, {
	SelectionMode = "None",

	Options = {
		{ Id = "duplicate", Text = "Duplicate" },
		{ Id = "rename", Text = "Rename" },
		{ Id = "delete", Text = "Delete" },
	},
})

dropdown.Selected:Connect(function(option)
	print("Action:", option.Id)
end)

This is useful for context menus, action menus, overflow menus, and command lists.

Default Selection

For single-selection dropdowns:

local dropdown = Dropdown.new(Button, {
	Selected = "newest",

	Options = {
		{ Id = "rarity", Text = "Rarity" },
		{ Id = "newest", Text = "Newest" },
	},
})

For multiple-selection dropdowns:

local dropdown = Dropdown.new(Button, {
	SelectionMode = "Multiple",
	Selected = { "common", "rare" },

	Options = {
		{ Id = "common", Text = "Common" },
		{ Id = "rare", Text = "Rare" },
		{ Id = "epic", Text = "Epic" },
	},
})

Dynamic Options

Options can be replaced at runtime.

dropdown:SetOptions({
	{ Id = "one", Text = "One" },
	{ Id = "two", Text = "Two" },
	{ Id = "three", Text = "Three" },
})

The dropdown automatically rebuilds its entries and drops any selection values that no longer exist. Additional helpers are available:

dropdown:AddOption({ Id = "four", Text = "Four" })
dropdown:RemoveOption("two")
dropdown:ClearOptions()

Reading Selection

Single Selection

local selectedId = dropdown:GetSelected()

local option = dropdown:GetSelectedOption()
if option then
	print(option.Text)
	print(option.Value)
end

Multiple Selection

local selectedIds = dropdown:GetSelected()
local selectedOptions = dropdown:GetSelectedOptions()

Changing Selection

dropdown:SetSelected("rare")

For multiple-selection dropdowns:

dropdown:SetSelected("rare", true)
dropdown:SetSelected("epic", true)
dropdown:SetSelected("common", false)

Clear all selection with dropdown:ClearSelection().

Opening and Closing

dropdown:Open()
dropdown:Close()
dropdown:Toggle()

if dropdown:IsOpen() then
	print("Dropdown is open")
end

Trigger Behavior

By default, pressing the supplied trigger button toggles the dropdown. Disable that behavior with:

ToggleOnTrigger = false

You can then control the dropdown manually:

Button.Activated:Connect(function()
	dropdown:Open()
end)

Updating Trigger Text

Generated dropdowns do not modify the trigger’s text unless requested.

local dropdown = Dropdown.new(Button, {
	UpdateTriggerText = true,

	Options = {
		{ Id = "rarity", Text = "Rarity" },
		{ Id = "newest", Text = "Newest" },
	},
})

With 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.

Dropdown Placement

The dropdown can automatically position itself relative to its trigger.

Placement = "Auto"

Supported values: Auto, Bottom, Top, Left, Right.

Auto prefers placing the dropdown beneath the trigger and flips it upward when there is not enough room.

Alignment

Alignment = "Start"

Supported values: Start, Center, End.

Offset

Offset = Vector2.new(0, 6)

This adds spacing between the trigger and dropdown.

Automatic Flipping

Enabled by default:

AutoFlip = true

For example, a dropdown configured for "Bottom" can automatically open above its trigger if there is not enough room beneath it.

Screen Clamping

Enabled by default:

ClampToScreen = true
ScreenPadding = 8

This prevents the dropdown from extending beyond the visible UI area.

Automatic Sizing

Generated dropdowns size themselves from their option count.

Width = 220
MinWidth = 160
MaxHeight = 280

EntryHeight = 38
EntryPadding = 4
ContentPadding = 6

When 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.

Open/Close Animation

Generated dropdowns animate open and closed with a UIScale tween by default.

Animation = {
	Enabled = true,
	Duration = 0.12,
	ClosedScale = 0.96,
	EasingStyle = Enum.EasingStyle.Quad,
	EasingDirection = Enum.EasingDirection.Out,
}

Set 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.

Styling Generated UI

The generated dropdown exposes property tables for its main visual states.

local dropdown = Dropdown.new(Button, {
	Style = {
		Container = {
			BackgroundColor3 = Color3.fromRGB(20, 22, 26),
		},

		Entry = {
			BackgroundColor3 = Color3.fromRGB(35, 38, 44),
			TextColor3 = Color3.fromRGB(240, 240, 240),
			TextSize = 16,
		},

		EntrySelected = {
			BackgroundColor3 = Color3.fromRGB(60, 110, 230),
		},

		EntryDisabled = {
			BackgroundTransparency = 0.35,
			TextTransparency = 0.45,
		},
	},
})

These tables are applied directly to the relevant generated Roblox Instances. Invalid properties produce a warning rather than breaking the dropdown.

Custom Entry Templates

You can provide your own entry template.

local dropdown = Dropdown.new(Button, {
	EntryTemplate = EntryTemplate,
	Options = options,
})

The template is cloned once per option. If the entry itself is not a GuiButton, the module searches its descendants for one. For explicit control:

GetEntryButton = function(entry, option)
	return entry.Checkbox
end

Custom Entry Rendering

For complete visual control, provide RenderOption.

local dropdown = Dropdown.new(Button, {
	EntryTemplate = EntryTemplate,

	GetEntryButton = function(entry)
		return entry.Checkbox
	end,

	RenderOption = function(entry, option, state)
		entry.TextLabel.Text = option.Text
		entry.SelectedIndicator.Visible = state.Selected
	end,

	Options = options,
})

The renderer receives state.Selected, state.Disabled, and state.Index. The module owns state and interactions while your renderer owns appearance.

Custom Entry Creation

Instead of cloning EntryTemplate, entries can be created dynamically.

CreateEntry = function(option, index, dropdown)
	local button = Instance.new("TextButton")
	button.Size = UDim2.new(1, 0, 0, 40)
	button.Text = option.Text
	return button
end

The returned object must be a GuiObject.

Custom Dropdown Templates

The entire dropdown container can also be replaced.

local dropdown = Dropdown.new(Button, {
	DropdownTemplate = MyDropdownTemplate,
	EntryTemplate = MyEntryTemplate,
	Options = options,
})

The dropdown template is cloned for this controller. The module still manages visibility, positioning, outside-click detection, selection, option rendering, lifecycle, and exclusivity.

Entry Container

When using a custom dropdown template, tell the module where option entries should be parented:

EntryContainer = "Entries"

The name is searched recursively inside the dropdown. You can also provide the Instance directly, or use a resolver:

GetEntryContainer = function(dropdown)
	return dropdown.MainCategoryHolder.Scroll
end

If nothing is specified, the module attempts to find Entries, Scroll, or Content, and then falls back to the first ScrollingFrame.

Existing Dropdown Instances

Instead of cloning a template, the module can control an existing UI object.

local dropdown = Dropdown.new(Button, {
	DropdownInstance = ExistingDropdown,
	EntryContainer = ExistingDropdown.Scroll,
	EntryTemplate = EntryTemplate,
	Options = options,
})

The caller retains ownership of DropdownInstance. Calling dropdown:Destroy() does not destroy the supplied dropdown instance, and its original visibility state is restored.

Overlay / Portal Rendering

Generated 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:

Parent = MyOverlayFrame

Exclusive Dropdowns

By default:

Exclusive = true

When 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.

Closing Behavior

CloseOnSelect = true
CloseOnOutsideClick = true
CloseOnEscape = true

If CloseOnSelect is omitted, "Single" closes after selection, "None" closes after activation, and "Multiple" stays open.

Enabling / Disabling

dropdown:SetEnabled(false)

A disabled dropdown cannot be opened or interacted with, and closes immediately if it was open. Check the state with dropdown:IsEnabled().

Signals

Every controller exposes four signals.

Opened

dropdown.Opened:Connect(function()
	print("Opened")
end)

Closed

dropdown.Closed:Connect(function()
	print("Closed")
end)

Selected

Fires whenever an enabled option is activated.

dropdown.Selected:Connect(function(option, index)
	print(option.Id, index)
end)

This signal also works when SelectionMode = "None", making it useful for action menus.

SelectionChanged

Fires whenever managed selection changes.

dropdown.SelectionChanged:Connect(function(selected, selectedOptions)
	print(selected)
end)

For 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.

⚙️ API Reference

Dropdown.new

Dropdown.new(triggerButton: GuiButton, config: table?)

Creates a dropdown controller.

Visibility

dropdown:Open()
dropdown:Close()
dropdown:Toggle()
dropdown:IsOpen()

Enabled State

dropdown:SetEnabled(enabled)
dropdown:IsEnabled()

Options

dropdown:SetOptions(options)
dropdown:GetOptions()
dropdown:GetOption(id)

dropdown:AddOption(option)
dropdown:RemoveOption(id)
dropdown:ClearOptions()

Selection

dropdown:SetSelected(id, selected)
dropdown:GetSelected()

dropdown:GetSelectedOption()
dropdown:GetSelectedOptions()

dropdown:ClearSelection()

Refresh

dropdown:Refresh()

Re-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.

Destroy

dropdown:Destroy()

Disconnects all controller connections and destroys module-owned UI and signals. Always call Destroy() when a dropdown controller is permanently no longer needed.

Configuration Reference

A representative configuration looks like:

local dropdown = Dropdown.new(Button, {
	Options = {},

	SelectionMode = "Single",
	Selected = nil,

	Enabled = true,
	ToggleOnTrigger = true,

	CloseOnSelect = nil,
	CloseOnOutsideClick = true,
	CloseOnEscape = true,

	Exclusive = true,

	Placement = "Auto",
	Alignment = "Start",
	Offset = Vector2.new(0, 6),

	ScreenPadding = 8,
	AutoFlip = true,
	ClampToScreen = true,

	Width = nil,
	MinWidth = 160,
	MaxHeight = 280,

	EntryHeight = 38,
	EntryPadding = 4,
	ContentPadding = 6,

	ZIndex = 100,

	UpdateTriggerText = false,

	Animation = {
		Enabled = true,
		Duration = 0.12,
		ClosedScale = 0.96,
		EasingStyle = Enum.EasingStyle.Quad,
		EasingDirection = Enum.EasingDirection.Out,
	},

	Style = {
		Container = {},
		Entry = {},
		EntrySelected = {},
		EntryDisabled = {},
	},

	-- Optional custom UI:
	Parent = nil,
	DropdownInstance = nil,
	DropdownTemplate = nil,
	EntryContainer = nil,
	EntryTemplate = nil,

	-- Optional callbacks:
	GetEntryContainer = nil,
	GetEntryButton = nil,
	GetEntryLabel = nil,
	CreateEntry = nil,
	RenderOption = nil,
	RenderTrigger = nil,
	OnEntryCreated = nil,
	OnEntryDestroyed = nil,
})

You only need to specify values that differ from the defaults.

Custom Trigger Rendering

Instead of UpdateTriggerText, provide your own renderer.

RenderTrigger = function(trigger, selectedOptions, dropdown)
	if #selectedOptions == 0 then
		trigger.Text = "Select rarity"
	else
		trigger.Text = selectedOptions[1].Text
	end
end

For multi-selection:

RenderTrigger = function(trigger, selectedOptions)
	trigger.Text = string.format("Filters (%d)", #selectedOptions)
end

Entry Lifecycle Hooks

Integrate other UI systems without making them dependencies of Dropdown.

OnEntryCreated = function(entry, option, dropdown)
	GuiButtonEffects:Setup(entry)
end,

OnEntryDestroyed = function(entry, option, dropdown)
	GuiButtonEffects:Clear(entry)
end,

This makes it possible to use Dropdown alongside custom effect, sound, or accessibility systems while keeping the module completely standalone.

Example: Sort Dropdown

local sortDropdown = Dropdown.new(SortButton, {
	UpdateTriggerText = true,
	Selected = "newest",

	Options = {
		{ Id = "rarity", Text = "Rarity", Value = "Rarity" },
		{ Id = "newest", Text = "Newest", Value = "Newest" },
		{ Id = "game", Text = "Game", Value = "Game" },
	},
})

sortDropdown.Selected:Connect(function(option)
	CurrentSort = option.Value
	refreshCollectibles()
end)

Example: Filter Dropdown

local rarityDropdown = Dropdown.new(FilterButton, {
	SelectionMode = "Multiple",
	CloseOnSelect = false,

	Options = {
		{ Id = 1, Text = "Common", Value = 1 },
		{ Id = 2, Text = "Uncommon", Value = 2 },
		{ Id = 3, Text = "Rare", Value = 3 },
		{ Id = 4, Text = "Epic", Value = 4 },
		{ Id = 5, Text = "Legendary", Value = 5 },
	},
})

rarityDropdown.SelectionChanged:Connect(function(_, selectedOptions)
	local rarities = {}

	for _, option in ipairs(selectedOptions) do
		table.insert(rarities, option.Value)
	end

	applyRarityFilter(rarities)
end)

Example: Custom Studio UI

Suppose your existing UI looks like:

CollectiblesSortDropdown
└── MainCategoryHolder
    └── Scroll

DropdownEntryTemplate
├── TextLabel
└── Checkbox

You can use it without changing the structure:

local dropdown = Dropdown.new(SortButton, {
	DropdownInstance = CollectiblesSortDropdown,
	EntryContainer = CollectiblesSortDropdown.MainCategoryHolder.Scroll,
	EntryTemplate = DropdownEntryTemplate,

	GetEntryButton = function(entry)
		return entry.Checkbox
	end,

	RenderOption = function(entry, option, state)
		entry.TextLabel.Text = option.Text
		entry.Checkbox.BackgroundColor3 = state.Selected
			and Color3.fromRGB(31, 173, 41)
			or Color3.fromRGB(31, 141, 173)
	end,

	Options = {
		{ Id = "rarity", Text = "Rarity" },
		{ Id = "newest", Text = "Newest" },
		{ Id = "game", Text = "Game" },
	},
})

This is the intended split: Dropdown owns behavior and state, your UI owns presentation.

Design Goals

Dropdown is designed around a few principles.

Data should own dropdown state

Options and selection live in the controller rather than relying on UI names or Attributes as application state.

UI should remain replaceable

The default renderer is intended to make the module usable immediately, not to force a particular visual design.

Every dropdown should be independent

Each controller owns its own connections, entries, options, selection, enabled state, and visibility. There is no singleton dropdown state.

External systems should remain optional

Button-effect systems, sound systems, scrolling utilities, and other frameworks can integrate through hooks without becoming required dependencies.

Showcase

The included showcase creates its complete interface dynamically with Instance.new. It demonstrates generated single-selection dropdowns, multi-selection filters, action menus, dynamic datasets, disabled entries, custom entry templates, and custom dropdown containers.

To run it:

ReplicatedStorage
└── Dropdown

StarterPlayer
└── StarterPlayerScripts
    └── DropdownShowcase

Place the supplied files in those locations and press Play.

📝 Notes

  • Only Id is required for the dropdown’s internal selection system — Text and Value are convenience fields.
  • IDs should be unique within a dropdown; a duplicate Id passed to SetOptions throws an error.
  • If Text is omitted, the module derives it from Name, then Value, then Id.
  • Disabled options cannot be selected and are excluded from the hover/press interactions of custom activation logic.
  • Generated dropdowns size themselves from the option count and become scrollable once content exceeds MaxHeight.
  • A dropdown’s trigger must be a descendant of a ScreenGui unless a custom Parent or DropdownInstance is supplied.
  • A custom DropdownInstance remains owned by the caller and is not destroyed by Destroy(); its original visibility is restored instead.
  • The module is intended to be required and used from client-side UI code.

License

Choose and include the license you want to distribute the module under before publishing it publicly.


made with ❤️ by biotoxin495

Package Details

Install command (Click to copy)


Version

1.0.1

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.