Tutorial · Menus and sound

How to make a settings menu in Roblox Studio

A settings menu is a small thing players notice when it is missing: no way to turn off the music, no way to make the game run better on an old phone. This guide builds a panel of on/off switches for music, sound effects and shadows. Each switch changes the game on that player's device straight away, and the server saves the choice so it is still there next time they join.

Updated 23 October 20265 min readIntermediateLuau

In short

Store each setting as an attribute on the Player, such as Setting_Music. A server Script loads saved values from a DataStore into those attributes on join, accepts changes through a RemoteEvent (only known names and true or false), and saves on leave. A LocalScript draws the switches, applies each setting on the device, and re-applies whenever the attribute changes.

  • Settings like music volume and shadows are applied by a LocalScript, so they only change that player's game.
  • The server keeps a list of allowed settings and their defaults, and ignores anything else a client sends.
  • If loading failed, do not save: an empty load must never overwrite a player's real settings.

Get the script

Settings menu with music and shadow switches

A LocalScript with a Settings button and a panel of switches for music, sound effects and shadows that apply at once on the player's device.

LocalScript in StarterPlayerScripts named SettingsMenu
-- LocalScript in StarterPlayerScripts named SettingsMenu.
-- A gear button opens a panel of on/off switches. Each switch changes the game on
-- this device straight away, and the server saves it for next time.
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")

local changeEvent = ReplicatedStorage:WaitForChild("ChangeSetting") :: RemoteEvent
local player = Players.LocalPlayer

-- The switches in the order they appear, with the text shown
local SETTINGS = {
	{ name = "Music", label = "Music" },
	{ name = "SoundEffects", label = "Sound effects" },
	{ name = "Shadows", label = "Shadows" },
}

-- What each setting does on this device. Changes made by a LocalScript stay on this device.
local function apply(name: string, on: boolean)
	if name == "Music" then
		local music = SoundService:FindFirstChild("Music")
		if music then
			for _, sound in music:GetChildren() do
				if sound:IsA("Sound") then
					sound.Volume = if on then 0.4 else 0
				end
			end
		end
	elseif name == "SoundEffects" then
		local effects = SoundService:FindFirstChild("Effects")
		if effects and effects:IsA("SoundGroup") then
			effects.Volume = if on then 0.5 else 0
		end
	elseif name == "Shadows" then
		Lighting.GlobalShadows = on
	end
end

local function get(name: string): boolean
	return player:GetAttribute("Setting_" .. name) ~= false
end

-- The gear button and the panel
local gui = Instance.new("ScreenGui")
gui.Name = "SettingsGui"
gui.ResetOnSpawn = false
local function corner(parent: Instance, radius: number)
	local c = Instance.new("UICorner")
	c.CornerRadius = UDim.new(0, radius)
	c.Parent = parent
end
local gear = Instance.new("TextButton")
gear.Text = "Settings"
gear.Size = UDim2.fromOffset(100, 40)
gear.Position = UDim2.new(0, 12, 0.5, -52)
gear.BackgroundColor3 = Color3.fromRGB(70, 64, 90)
gear.TextColor3 = Color3.new(1, 1, 1)
gear.Font = Enum.Font.GothamBold
gear.TextSize = 16
corner(gear, 10)
gear.Parent = gui

local panel = Instance.new("Frame")
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.5)
panel.Size = UDim2.fromOffset(340, 60 + #SETTINGS * 52)
panel.BackgroundColor3 = Color3.fromRGB(30, 26, 40)
panel.Visible = false
corner(panel, 14)
panel.Parent = gui
local heading = Instance.new("TextLabel")
heading.Text = "Settings"
heading.Size = UDim2.new(1, 0, 0, 48)
heading.BackgroundTransparency = 1
heading.TextColor3 = Color3.new(1, 1, 1)
heading.Font = Enum.Font.GothamBold
heading.TextSize = 20
heading.Parent = panel

local switches: { [string]: TextButton } = {}
local function drawSwitch(name: string)
	local on = get(name)
	local switch = switches[name]
	switch.Text = if on then "ON" else "OFF"
	switch.BackgroundColor3 = if on then Color3.fromRGB(70, 200, 110) else Color3.fromRGB(90, 84, 104)
end

for i, setting in SETTINGS do
	local row = Instance.new("TextLabel")
	row.Text = setting.label
	row.Size = UDim2.new(1, -120, 0, 44)
	row.Position = UDim2.fromOffset(20, 48 + (i - 1) * 52)
	row.BackgroundTransparency = 1
	row.TextColor3 = Color3.new(1, 1, 1)
	row.Font = Enum.Font.Gotham
	row.TextSize = 17
	row.TextXAlignment = Enum.TextXAlignment.Left
	row.Parent = panel
	local switch = Instance.new("TextButton")
	switch.Size = UDim2.fromOffset(80, 36)
	switch.Position = UDim2.new(1, -100, 0, 52 + (i - 1) * 52)
	switch.TextColor3 = Color3.new(1, 1, 1)
	switch.Font = Enum.Font.GothamBold
	switch.TextSize = 16
	corner(switch, 18)
	switch.Parent = panel
	switches[setting.name] = switch
	drawSwitch(setting.name)
	switch.Activated:Connect(function()
		local on = not get(setting.name)
		-- Change it here at once; the server checks it, stores it and saves it
		player:SetAttribute("Setting_" .. setting.name, on)
		changeEvent:FireServer(setting.name, on)
	end)
	-- Redraw and apply whenever the value changes, including when the server loads it
	player:GetAttributeChangedSignal("Setting_" .. setting.name):Connect(function()
		drawSwitch(setting.name)
		apply(setting.name, get(setting.name))
	end)
	apply(setting.name, get(setting.name))
end

gear.Activated:Connect(function()
	panel.Visible = not panel.Visible
end)
gui.Parent = player:WaitForChild("PlayerGui")

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: the saver and the menu
ServerScriptService
  SettingsSaver Script new
StarterPlayer
  StarterPlayerScripts
    SettingsMenu LocalScript new

Settings you can change

SETTINGSIn the script
The switches in the menu, in order, with their labels. Each name must also be in the server's DEFAULTS.

Saving needs the SettingsSaver Script from Step 1 and Enable Studio Access to API Services to test in Studio. The Music switch works with the folder from the music player guide; Sound effects needs a SoundGroup named Effects in SoundService.

Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add a settings menu: a Settings button that opens a panel with ON/OFF switches for Music, Sound effects and Shadows, applied on the player's device right away, stored as attributes on the player, and saved by a server Script in a DataStore that only accepts known settings with true or false values.

On this page
  1. Get the script
  2. How it works
  3. 1. Server
  4. 2. Menu
  5. Common errors
  6. With RoCode
  7. Questions
  8. Sources

How the settings menu works

The settings and what they change on the device
SettingChangesWhy it helps
MusicVolume of every Sound in SoundService.MusicPlayers who listen to their own music.
Sound effectsVolume of the Effects SoundGroupQuiet play.
ShadowsLighting.GlobalShadowsFaster frames on older phones.

Attributes on the Player carry the values. When the server sets one, it reaches the player's device, and GetAttributeChangedSignal lets the menu redraw and apply it. When the player flips a switch, the menu sets the attribute on its own copy for an instant response and tells the server, which checks it and stores it.

Step 1: Load and save settings on the server

Turn on Enable Studio Access to API Services in Game Settings, Security (the place must be published), then insert a Script in ServerScriptService named SettingsSaver:

Script in ServerScriptService named SettingsSaver
-- Script in ServerScriptService named SettingsSaver.
-- Loads each player's settings into attributes on the player, and saves the changes
-- the settings menu sends. Only known settings with the right type are kept.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Every setting and its default. The menu shows these.
local DEFAULTS: { [string]: boolean } = {
	Music = true,
	SoundEffects = true,
	Shadows = true,
}

local store = DataStoreService:GetDataStore("PlayerSettings_v1")
local changeEvent = Instance.new("RemoteEvent")
changeEvent.Name = "ChangeSetting"
changeEvent.Parent = ReplicatedStorage

local dirty: { [Player]: boolean } = {} -- changed since the last save
local loaded: { [Player]: boolean } = {}

local function current(player: Player): { [string]: boolean }
	local values: { [string]: boolean } = {}
	for name, default in DEFAULTS do
		local value = player:GetAttribute("Setting_" .. name)
		values[name] = if type(value) == "boolean" then value else default
	end
	return values
end

Players.PlayerAdded:Connect(function(player: Player)
	local ok, data = pcall(function()
		return store:GetAsync(tostring(player.UserId))
	end)
	local saved = if ok and type(data) == "table" then data else {}
	for name, default in DEFAULTS do
		local value = saved[name]
		player:SetAttribute("Setting_" .. name, if type(value) == "boolean" then value else default)
	end
	-- Only save later if the load worked, so a failed load never overwrites good settings
	loaded[player] = ok
end)

changeEvent.OnServerEvent:Connect(function(player: Player, name: unknown, value: unknown)
	if typeof(name) ~= "string" or DEFAULTS[name] == nil or typeof(value) ~= "boolean" then
		return
	end
	player:SetAttribute("Setting_" .. name, value)
	dirty[player] = true
end)

local function save(player: Player)
	if not (dirty[player] and loaded[player]) then
		return
	end
	dirty[player] = nil
	local ok, err = pcall(function()
		store:SetAsync(tostring(player.UserId), current(player))
	end)
	if not ok then
		warn("SettingsSaver: could not save", player.Name, err)
	end
end

Players.PlayerRemoving:Connect(function(player: Player)
	save(player)
	dirty[player] = nil
	loaded[player] = nil
end)

game:BindToClose(function()
	for _, player in Players:GetPlayers() do
		task.spawn(save, player)
	end
	task.wait(3)
end)

DEFAULTS is the list of allowed settings. A change for a name not in it, or a value that is not true or false, is ignored. Settings save only when they changed and only if the load worked. BindToClose saves everyone when a server shuts down.

Step 2: The menu

  1. For the Sound effects switch, insert a SoundGroup in SoundService named Effects, and set the SoundGroup property of your effect Sounds to it.
  2. Insert a LocalScript in StarterPlayer > StarterPlayerScripts named SettingsMenu:
LocalScript in StarterPlayerScripts named SettingsMenu
-- LocalScript in StarterPlayerScripts named SettingsMenu.
-- A gear button opens a panel of on/off switches. Each switch changes the game on
-- this device straight away, and the server saves it for next time.
local Lighting = game:GetService("Lighting")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService = game:GetService("SoundService")

local changeEvent = ReplicatedStorage:WaitForChild("ChangeSetting") :: RemoteEvent
local player = Players.LocalPlayer

-- The switches in the order they appear, with the text shown
local SETTINGS = {
	{ name = "Music", label = "Music" },
	{ name = "SoundEffects", label = "Sound effects" },
	{ name = "Shadows", label = "Shadows" },
}

-- What each setting does on this device. Changes made by a LocalScript stay on this device.
local function apply(name: string, on: boolean)
	if name == "Music" then
		local music = SoundService:FindFirstChild("Music")
		if music then
			for _, sound in music:GetChildren() do
				if sound:IsA("Sound") then
					sound.Volume = if on then 0.4 else 0
				end
			end
		end
	elseif name == "SoundEffects" then
		local effects = SoundService:FindFirstChild("Effects")
		if effects and effects:IsA("SoundGroup") then
			effects.Volume = if on then 0.5 else 0
		end
	elseif name == "Shadows" then
		Lighting.GlobalShadows = on
	end
end

local function get(name: string): boolean
	return player:GetAttribute("Setting_" .. name) ~= false
end

-- The gear button and the panel
local gui = Instance.new("ScreenGui")
gui.Name = "SettingsGui"
gui.ResetOnSpawn = false
local function corner(parent: Instance, radius: number)
	local c = Instance.new("UICorner")
	c.CornerRadius = UDim.new(0, radius)
	c.Parent = parent
end
local gear = Instance.new("TextButton")
gear.Text = "Settings"
gear.Size = UDim2.fromOffset(100, 40)
gear.Position = UDim2.new(0, 12, 0.5, -52)
gear.BackgroundColor3 = Color3.fromRGB(70, 64, 90)
gear.TextColor3 = Color3.new(1, 1, 1)
gear.Font = Enum.Font.GothamBold
gear.TextSize = 16
corner(gear, 10)
gear.Parent = gui

local panel = Instance.new("Frame")
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.5)
panel.Size = UDim2.fromOffset(340, 60 + #SETTINGS * 52)
panel.BackgroundColor3 = Color3.fromRGB(30, 26, 40)
panel.Visible = false
corner(panel, 14)
panel.Parent = gui
local heading = Instance.new("TextLabel")
heading.Text = "Settings"
heading.Size = UDim2.new(1, 0, 0, 48)
heading.BackgroundTransparency = 1
heading.TextColor3 = Color3.new(1, 1, 1)
heading.Font = Enum.Font.GothamBold
heading.TextSize = 20
heading.Parent = panel

local switches: { [string]: TextButton } = {}
local function drawSwitch(name: string)
	local on = get(name)
	local switch = switches[name]
	switch.Text = if on then "ON" else "OFF"
	switch.BackgroundColor3 = if on then Color3.fromRGB(70, 200, 110) else Color3.fromRGB(90, 84, 104)
end

for i, setting in SETTINGS do
	local row = Instance.new("TextLabel")
	row.Text = setting.label
	row.Size = UDim2.new(1, -120, 0, 44)
	row.Position = UDim2.fromOffset(20, 48 + (i - 1) * 52)
	row.BackgroundTransparency = 1
	row.TextColor3 = Color3.new(1, 1, 1)
	row.Font = Enum.Font.Gotham
	row.TextSize = 17
	row.TextXAlignment = Enum.TextXAlignment.Left
	row.Parent = panel
	local switch = Instance.new("TextButton")
	switch.Size = UDim2.fromOffset(80, 36)
	switch.Position = UDim2.new(1, -100, 0, 52 + (i - 1) * 52)
	switch.TextColor3 = Color3.new(1, 1, 1)
	switch.Font = Enum.Font.GothamBold
	switch.TextSize = 16
	corner(switch, 18)
	switch.Parent = panel
	switches[setting.name] = switch
	drawSwitch(setting.name)
	switch.Activated:Connect(function()
		local on = not get(setting.name)
		-- Change it here at once; the server checks it, stores it and saves it
		player:SetAttribute("Setting_" .. setting.name, on)
		changeEvent:FireServer(setting.name, on)
	end)
	-- Redraw and apply whenever the value changes, including when the server loads it
	player:GetAttributeChangedSignal("Setting_" .. setting.name):Connect(function()
		drawSwitch(setting.name)
		apply(setting.name, get(setting.name))
	end)
	apply(setting.name, get(setting.name))
end

gear.Activated:Connect(function()
	panel.Visible = not panel.Visible
end)
gui.Parent = player:WaitForChild("PlayerGui")

apply is where each setting does its job. Changes a LocalScript makes to Lighting and sounds stay on that device, so turning shadows off for a slow phone does not change anyone else's game. To add a setting, add it to the server's DEFAULTS, to SETTINGS here, and a branch in apply.

Test it

  1. Press Play and press Settings. Three switches show ON.
  2. Turn Shadows off: shadows disappear at once. Turn Music off: the music goes quiet.
  3. Stop, press Play again and open Settings: your choices are still there.

Common errors and fixes

Check the Output window on the Server view for saving errors.

Settings reset every time I play

Saving needs Enable Studio Access to API Services in Studio. Check the Output for "could not save".

Infinite yield possible on 'ReplicatedStorage:WaitForChild("ChangeSetting")'

SettingsSaver is not running. It must be a Script in ServerScriptService.

The Sound effects switch does nothing

Your effects need to be in a SoundGroup named Effects in SoundService: set each Sound's SoundGroup property to it.

Music turns back on when a new song starts

Another music script sets the volume when it plays a song. Have it read Setting_Music before setting the volume.

Build it with RoCode

If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and GUI in your Explorer.

You type

Add a settings menu with switches for music, sound effects, shadows and a low graphics mode, saved between sessions.

RoCode does
  • Can insert a ready-made, animated settings screen from its premium UI kits in one step, in one of five themes (candy, neon, gold, blocky, pastel), with working server scripts
  • Or writes the saver and menu from this guide and wires them to your music and sounds
  • Is told to compile-check each script with Check Script, and sets undo points around each batch

After a build, RoCode can run its test robot: it plays your game in Studio, clicks the buttons and reports PASS or FAIL. It checks only what it chose to test. How RoCode connects to Studio.

Questions

How do I make a settings menu in Roblox?

Keep each setting as a Player attribute, apply it in a LocalScript, and let a server Script check and save it in a DataStore. Steps 1 and 2 have both scripts.

How do I let players turn off music?

Play music from a LocalScript, then a switch can set its volume to 0 on that player's device only.

Can a settings menu make my game run faster?

Turning off Lighting.GlobalShadows on a device can help on slower phones. Players can also lower graphics quality in Roblox's own menu.

Why save settings on the server?

So they follow the player to any device. A LocalScript cannot write to a DataStore; only the server can.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.