In short
On the server, get the character's current look with humanoid:GetAppliedDescription(), change the properties you want (such as HeadColor and the other body colours, HeightScale or Shirt), then call humanoid:ApplyDescriptionAsync(description). A RemoteEvent carries the player's choice from a menu; the server only accepts values from its own lists.
- Start from the applied description, not a new one, or the character loses everything you did not set.
- Apply on the server so every player sees the new look.
- Only accept colours, sizes and asset IDs from server-side lists, so nobody can wear anything you did not offer.
Get the script
Character customization server script
A Script in ServerScriptService that changes a player's skin colour, height, shirt or pants from allowed lists with HumanoidDescription.
-- Script in ServerScriptService named Customization.
-- The client asks for a change; the server only accepts colours, sizes and clothing
-- from the lists below, then applies them with a HumanoidDescription.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- What players may choose. Add clothing asset IDs from the Creator Store or Marketplace.
local SKIN_COLORS: { Color3 } = {
Color3.fromRGB(255, 220, 177),
Color3.fromRGB(234, 184, 146),
Color3.fromRGB(198, 134, 66),
Color3.fromRGB(141, 85, 36),
Color3.fromRGB(90, 56, 37),
Color3.fromRGB(110, 170, 255),
}
local SHIRTS: { number } = { 0 } -- 0 means no shirt; add shirt asset IDs here
local PANTS: { number } = { 0 }
local MIN_HEIGHT, MAX_HEIGHT = 0.9, 1.1 -- R15 height scale range
local COOLDOWN = 1 -- seconds between changes
local changeEvent = Instance.new("RemoteEvent")
changeEvent.Name = "ChangeLook"
changeEvent.Parent = ReplicatedStorage
local lastChange: { [Player]: number } = {}
local function setAllBodyColors(description: HumanoidDescription, color: Color3)
description.HeadColor = color
description.TorsoColor = color
description.LeftArmColor = color
description.RightArmColor = color
description.LeftLegColor = color
description.RightLegColor = color
end
changeEvent.OnServerEvent:Connect(function(player: Player, kind: unknown, value: unknown)
if typeof(kind) ~= "string" or typeof(value) ~= "number" or value ~= value then
return
end
local now = os.clock()
if now - (lastChange[player] or -math.huge) < COOLDOWN then
return
end
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health <= 0 then
return
end
lastChange[player] = now
-- Start from what the character wears now, and change one thing
local description = humanoid:GetAppliedDescription()
local index = math.floor(value)
if kind == "skin" and SKIN_COLORS[index] then
setAllBodyColors(description, SKIN_COLORS[index])
elseif kind == "shirt" and SHIRTS[index] then
description.Shirt = SHIRTS[index]
elseif kind == "pants" and PANTS[index] then
description.Pants = PANTS[index]
elseif kind == "height" then
description.HeightScale = math.clamp(value, MIN_HEIGHT, MAX_HEIGHT)
else
return
end
local ok, err = pcall(function()
humanoid:ApplyDescriptionAsync(description)
end)
if not ok then
warn("Customization: could not apply look for", player.Name, err)
end
end)
Players.PlayerRemoving:Connect(function(player: Player)
lastChange[player] = nil
end)
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
ServerScriptService
Customization Script new
StarterPlayer
StarterPlayerScripts
CustomizeMenu LocalScript new
Settings you can change
SHIRTSIn the script- Shirt asset IDs players may choose; 0 means no shirt. PANTS works the same way. Default:
{ 0 }. MIN_HEIGHT, MAX_HEIGHTIn the script- The range of the height scale for R15 characters. Default:
0.9, 1.1. COOLDOWNIn the script- Seconds between changes for one player. Default:
1.
What a cheater can and cannot doThe client sends only a choice number or a height. The server picks the colour or asset ID from its own lists, clamps the height and limits how often a player can change, so a cheater cannot put on items you did not offer.
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 character customization: a menu with 6 skin colour swatches and Short, Normal and Tall buttons, and a server Script that takes the choice through a RemoteEvent, changes the player's HumanoidDescription from GetAppliedDescription (all body colours, or HeightScale clamped between 0.9 and 1.1), and applies it with ApplyDescriptionAsync.
On this page
How HumanoidDescription works
A HumanoidDescription lists everything about a character's look. GetAppliedDescription returns a copy of the one the character has now; you change the copy and apply it. Roblox then updates only what changed. The older ApplyDescription is superseded by ApplyDescriptionAsync for new work, which is what this guide uses.
| Property | What it is |
|---|---|
HeadColor, TorsoColor, LeftArmColor and the rest | Body part colours, as Color3. |
HeightScale | Height for R15 characters. Scale properties do not affect R6. |
Shirt, Pants | Clothing asset IDs; 0 removes the item. |
Step 1: The server script
Insert a Script in ServerScriptService named Customization:
-- Script in ServerScriptService named Customization.
-- The client asks for a change; the server only accepts colours, sizes and clothing
-- from the lists below, then applies them with a HumanoidDescription.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- What players may choose. Add clothing asset IDs from the Creator Store or Marketplace.
local SKIN_COLORS: { Color3 } = {
Color3.fromRGB(255, 220, 177),
Color3.fromRGB(234, 184, 146),
Color3.fromRGB(198, 134, 66),
Color3.fromRGB(141, 85, 36),
Color3.fromRGB(90, 56, 37),
Color3.fromRGB(110, 170, 255),
}
local SHIRTS: { number } = { 0 } -- 0 means no shirt; add shirt asset IDs here
local PANTS: { number } = { 0 }
local MIN_HEIGHT, MAX_HEIGHT = 0.9, 1.1 -- R15 height scale range
local COOLDOWN = 1 -- seconds between changes
local changeEvent = Instance.new("RemoteEvent")
changeEvent.Name = "ChangeLook"
changeEvent.Parent = ReplicatedStorage
local lastChange: { [Player]: number } = {}
local function setAllBodyColors(description: HumanoidDescription, color: Color3)
description.HeadColor = color
description.TorsoColor = color
description.LeftArmColor = color
description.RightArmColor = color
description.LeftLegColor = color
description.RightLegColor = color
end
changeEvent.OnServerEvent:Connect(function(player: Player, kind: unknown, value: unknown)
if typeof(kind) ~= "string" or typeof(value) ~= "number" or value ~= value then
return
end
local now = os.clock()
if now - (lastChange[player] or -math.huge) < COOLDOWN then
return
end
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health <= 0 then
return
end
lastChange[player] = now
-- Start from what the character wears now, and change one thing
local description = humanoid:GetAppliedDescription()
local index = math.floor(value)
if kind == "skin" and SKIN_COLORS[index] then
setAllBodyColors(description, SKIN_COLORS[index])
elseif kind == "shirt" and SHIRTS[index] then
description.Shirt = SHIRTS[index]
elseif kind == "pants" and PANTS[index] then
description.Pants = PANTS[index]
elseif kind == "height" then
description.HeightScale = math.clamp(value, MIN_HEIGHT, MAX_HEIGHT)
else
return
end
local ok, err = pcall(function()
humanoid:ApplyDescriptionAsync(description)
end)
if not ok then
warn("Customization: could not apply look for", player.Name, err)
end
end)
Players.PlayerRemoving:Connect(function(player: Player)
lastChange[player] = nil
end)
The client sends a kind ("skin", "shirt", "pants" or "height") and a number. For colours and clothing the number is a position in the server's list, so a request for item 99 finds nothing and is ignored. Height is clamped to the allowed range. ApplyDescriptionAsync waits while Roblox loads any new assets and can fail, so it runs inside pcall.
Step 2: The customize menu
Insert a LocalScript in StarterPlayer > StarterPlayerScripts named CustomizeMenu:
-- LocalScript in StarterPlayerScripts named CustomizeMenu.
-- Skin colour swatches and height buttons. Each click asks the server to change the look.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local changeEvent = ReplicatedStorage:WaitForChild("ChangeLook") :: RemoteEvent
-- The same colours as the server's SKIN_COLORS, in the same order, for the swatches
local SWATCHES: { Color3 } = {
Color3.fromRGB(255, 220, 177),
Color3.fromRGB(234, 184, 146),
Color3.fromRGB(198, 134, 66),
Color3.fromRGB(141, 85, 36),
Color3.fromRGB(90, 56, 37),
Color3.fromRGB(110, 170, 255),
}
local HEIGHTS = { 0.9, 1, 1.1 }
local gui = Instance.new("ScreenGui")
gui.Name = "CustomizeGui"
gui.ResetOnSpawn = false
local panel = Instance.new("Frame")
panel.AnchorPoint = Vector2.new(0, 1)
panel.Position = UDim2.new(0, 12, 1, -12)
panel.Size = UDim2.fromOffset(300, 120)
panel.BackgroundColor3 = Color3.fromRGB(30, 26, 40)
panel.Parent = gui
local panelCorner = Instance.new("UICorner")
panelCorner.CornerRadius = UDim.new(0, 12)
panelCorner.Parent = panel
local function addButton(position: UDim2, size: UDim2, color: Color3, text: string, onClick: () -> ())
local b = Instance.new("TextButton")
b.Position = position
b.Size = size
b.BackgroundColor3 = color
b.Text = text
b.TextColor3 = Color3.new(1, 1, 1)
b.Font = Enum.Font.GothamBold
b.TextSize = 14
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, 8)
c.Parent = b
b.Activated:Connect(onClick)
b.Parent = panel
end
for i, color in SWATCHES do
addButton(UDim2.fromOffset(12 + (i - 1) * 46, 14), UDim2.fromOffset(38, 38), color, "", function()
changeEvent:FireServer("skin", i)
end)
end
for i, height in HEIGHTS do
local label = if height < 1 then "Short" elseif height > 1 then "Tall" else "Normal"
addButton(UDim2.fromOffset(12 + (i - 1) * 94, 66), UDim2.fromOffset(86, 40), Color3.fromRGB(110, 80, 220), label, function()
changeEvent:FireServer("height", height)
end)
end
gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
The swatches use the same colours in the same order as the server's list, and send their position. Keep the two lists in step when you change one. For a longer menu, see the shop GUI guide for scrolling lists and the UI animations guide for hover effects.
Step 3: Keep the look after a respawn
A respawn builds a new character from the player's own avatar, so changes are lost. To keep them, store the player's choices on the server (in a table, or attributes on the Player), and in player.CharacterAdded apply them again with the same code. To keep them between sessions too, save the choices with the DataStore guide.
Test it
- Press Play. Click a swatch: after a moment your character's skin changes colour.
- Click Tall: your character grows. With an R6 avatar nothing happens, as scales only affect R15.
- Start a 2-player test: the other player sees your new look.
- Click swatches very fast: changes apply at most once a second.
Common errors and fixes
Check the Output window on the Server view for warnings from Customization.
Customization: could not apply look
ApplyDescriptionAsync failed, often because an asset ID in the description does not exist or could not load. Check the IDs in SHIRTS and PANTS.
Height does nothing
The character is R6. Set the game to R15 in Game Settings, Avatar, or use colours and clothing only.
My hat and hair disappeared
The description was built with Instance.new("HumanoidDescription") instead of GetAppliedDescription(). Always start from the applied one.
The change only shows for me
It was applied by a LocalScript. Apply it on the server, as Customization does.
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.
Add a character customizer with skin colours, three heights and four shirts I give you the IDs for, applied on the server and kept after respawning.
- Writes the server script with your allowed lists and the menu that matches them
- Is told to compile-check each script with Check Script, and sets undo points around each batch
- Can save the choices with your existing data script if you have one
RoCode does not choose or upload clothing for you: pick the asset IDs yourself. After a build its test robot can click the menu in Studio and report PASS or FAIL, but it checks only what it chose to test. How RoCode connects to Studio.
Questions
How do I change a player's character in Roblox?
On the server, take humanoid:GetAppliedDescription(), change its properties, and apply it with humanoid:ApplyDescriptionAsync().
How do I change skin colour with a script?
Set HeadColor, TorsoColor, LeftArmColor, RightArmColor, LeftLegColor and RightLegColor on the description to the same Color3, then apply it.
Why does my HumanoidDescription change not work?
Apply it on the server, start from GetAppliedDescription, and check the Output: ApplyDescriptionAsync can fail on a bad asset ID.
Can players wear any item they type in?
Not with this script: the server only accepts IDs from its own lists. Letting players enter any ID means checking it is a real item of the right type first.
Sources and further reading
- Roblox Creator Docs: Character appearance. HumanoidDescription, its properties and applying it
- Roblox Creator Docs: Humanoid:ApplyDescriptionAsync. applying a description
- Roblox Creator Docs: HumanoidDescription. body colours, scales and clothing
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.