How to Add Custom Player Icons to Chat

Today I’ll be showing you how to edit the default Roblox chat to add icons next to a player’s name!


Forking the Chat

First, we’ll need to fork parts of the built-in chat. In Roblox Studio, start a test via Run or Play Solo. Navigate to the “Chat” object in the Explorer and select the ChatModules and ClientChatModules folders within:


Copy these folders, stop the test, and paste them back into the Chat object. Upon expanding these folders, you’ll find quite a lot of scripts! Fortunately, we only need three of them. In the ChatModules folder, delete all modules except for ExtraDataInitializer, which is how we’ll process the speakers and attach the icons to their messages. In the MessageCreatorModules folder (located within ClientChatModules) delete all modules except for DefaultChatMessage, which will be responsible for processing a message’s icon data, and Util, which will build the actual GUI elements. Make sure that the InsertDefaultModules bool values are still present and set to true. Your explorer should now look like this:


Editing ExtraDataInitializer

Alright, let’s start programming! Go ahead and open up the ExtraDataInitializer module. We’ll start by adding the dictionary of icons to reference later. I put it directly underneath the SpecialChatColors dictionary, since it serves a very similar purpose.

local SpecialChatIcons = { --the main dictionary
	Groups = { --a list of groups
			--- ChipioIndustries group
			GroupId = 2932328,
			Rank = 1,
			Icons = {"rbxasset://textures/ui/PlayerList/OwnerIcon.png"},
		}, --we'll also be setting up the system in a way that
	},			--lets internal textures be used.
	Players = { --a list of players
			--- ChipioIndustries
			UserId = 18697683,
			Icons = {4081664234}, --Icons is a table because it
		},			--allows for multiple icons per user/group.

Up next, we need to edit the ConstructIsInGroups function. This function’s job is to iterate through the SpecialChatColors dictionary and apply a useful IsInGroup function to each entry. However, we need to modify it to work with our icon dictionary by adding the following lines to the bottom of the function:

	if SpecialChatIcons.Groups then --does our table exist?
		for _,group in pairs(SpecialChatIcons.Groups) do --iterate through it
			--there's an existing function called MakeIsInGroup
			--that we can use.
			group.IsInGroup = MakeIsInGroup(group.GroupId,group.Rank)

Now that we have a pool of data to draw from, we need to be able to figure out which icons should be attached to the player. Fortunately, there’s already a very similar function called GetSpecialChatColor that we can very closely replicate with our own GetSpecialChatIcons.

--there are two types of image IDs: ones with just the ID itself,
--and ones with the rest of the address already attached. If it's
--just a number, we need to attach the address to it, and if it's
--a full address, we can go ahead and return it.
local function ProcessImageID(ID)
	if tonumber(ID) then
		return "rbxassetid://"..tostring(ID)
		return ID

local function GetSpecialChatIcons(speakerName)
	local chatIcons={} --an empty list for us to add to
	if SpecialChatIcons.Players then --is there a players folder?
		local playerFromSpeaker=Players:FindFirstChild(speakerName)
		if playerFromSpeaker then --does the player exist?
			--now we're going to cycle through the icon entries
			--to see if we can find matching user IDs.
			for _,player in pairs(SpecialChatIcons.Players) do
				if playerFromSpeaker.UserId == player.UserId then
					--if there's a match, we can loop through the entry's
					--icons and add them to the list, passing them through
					--ProcessImageID first.
					for __,icon in pairs(player.Icons) do
	if SpecialChatIcons.Groups then --is there a groups folder?
		for _,group in pairs(SpecialChatIcons.Groups) do --cycle through groups
			--this line uses the IsInGroup function we applied earlier.
			if group.IsInGroup(Players:FindFirstChild(speakerName)) then
				--if we've found a match, add the icons to the table.
				for __,icon in pairs(group.Icons) do
	--finally, return the table.
	return chatIcons

Once we have a way to get the icons associated with a player, we need to do something with that information by attaching it to the speaker object as it’s created. There’s a function in the script called onNewSpeaker which we can edit to attach our own data. Anywhere in the function should work, but I’m adding it right after the closing of the “if” statement if not speaker:GetExtraData("Tags") then.

		--has the info been set already?
		if not speaker:GetExtraData("Icons") then
			--use our function to get a player's icons
			local specialIcons=GetSpecialChatIcons(speakerName)
			if specialIcons then --if our function worked,
				--we can apply it to the speaker with SetExtraData.

Editing DefaultChatMessage

Now we’ve successfully attached the data to the player! Now we just need to edit a couple other scripts to actually display the images. Go ahead and exit ExtraDataInitializer and open up DefaultChatMessage, located within the MessageCreatorModules folder. To keep with the design of what’s there, we’re going to create a variable for the icons right alongside the rest of the data near the top of the script.

	local icons = extraData.Icons or {}

Later on down the script, in the same are where the tags are created (for me, I started writing my code on line 55) we’ll want to create the icons.

	local iconLabels = {} --create a list for our image labels
	for i,icon in pairs(icons) do --cycle through the icons
		--this line uses a function we'll be creating in a moment.
		local label = util:AddIconLabelToBaseMessage(BaseMessage,icon)
		--guiObjectSpacing tracks where the next element should be
		--positioned. We can use this for our purposes.
		label.Position = guiObjectSpacing
		--Now we need to update the numNeededSpaces and guiObjectSpacing
		--variables to account for what we've added. We can just add
		--the result of another util function called GetNumberOfSpaces
		--to numNeededSpaces, passing through the icon label, as well as
		--two existing variables called useFont and useTextSize.
		numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(label,useFont,useTextSize)
		--guiObjectSpacing is much simpler, and we only need to add the
		--horizontal offset of the label size to the total.
		guiObjectSpacing = guiObjectSpacing +, label.Size.X.Offset, 0, 0)
		table.insert(iconLabels,label) --finally, we can add the label to the list.

So… what’s the point of adding the label objects to a list, anyway? Well, we’re actually going to iterate through the table later on in the script in order to specify how the labels fade in and out. Skip down to around line 100, right after the tag labels have been iterated through.

The FadeParmaters dictionary holds a list of items and their corresponding properties which need to be faded to specified values. The overall structure looks something like this:


	--loop through the icon labels
	for i, iconLabel in pairs(iconLabels) do
		--create a new entry in the existing FadeParmaters table
		--(misspelling and all) using the iconLabel itself as a key.
		--The only property we need to configure is ImageTransparency,
		--and we'll want the faded in state to be 0 transparency, and the
		--faded out state at 1.
		FadeParmaters[iconLabel] = {

Editing Util

Util holds a lot of essential functions, and we’ll want to edit some of those to work with our image labels. That, and one of the functions we used doesn’t even exist. At the top of the script, we can declare a constant for the image size:


Up next, we need to edit a function called GetNumberOfSpaces. You’ll recall that we used this to increase numNeededSpaces by passing through the label itself. Thing is, the function is designed for text, not labels. There’s one line in particular we want to replace:

	local strSize = self:GetStringTextBounds(str, font, textSize)

More specifically, we want to replace it with the following code:

	local strSize --define the variable in a scope that can be
	--accessed by later lines of code
	if typeof(str)=="string" then --is it a string?
		--if so, treat it like a string.
		strSize = self:GetStringTextBounds(str, font, textSize)
	elseif typeof(str)=="Instance" then --or is it an icon?
		--if so, we can just use the label's AbsoluteSize property.
		strSize = str.AbsoluteSize

Since GetStringTextBounds and AbsoluteSize both return Vector2 values, the rest of the function will have no problem processing the information. However, we haven’t actually written the code that will create the label–so let’s do that! I put my function around line 203, but it doesn’t matter very much.

--this is the function we used in DefaultChatMessage.
function methods:AddIconLabelToBaseMessage(BaseMessage, iconAddress)
	local iconSize=DEFAULT_ICON_SIZE --use the constant we defined earlier
	local IconLabel=self:GetFromObjectPool("ImageLabel") --see below.
	--set various properties of the IconLabel to what we want.
	--Assign the IconLabel the image we want.
	--Add it to the base message.
	--Return it to wherever the function was called from.
	return IconLabel

You may have noticed that we didn’t use to get an ImageLabel, but instead a function called GetFromObjectPool. This is a function included in the chat system that recycles previously-used objects instead of deleting them. However, because we’re using recycled objects, we need to be careful to not assume any of the object’s default properties, because they may have been changed by whatever they were used for previously.


I was inspired by a thread questioning about whether or not it was possible, and after reading a whole lot of documentation and getting it working, I figured it would be a fun thing to share. I learned a lot about editing existing programs, and I hope you did too!

Finally, if you really don’t want to edit the scripts yourself, I’ve posted a drag-and-drop solution as a free model, which you can get here:

I hope you’ve found this guide helpful! Let me know if you have any questions/issues.


10/24/2019: The part of the ExtraDataInitializer that applied the player’s icons was mistakenly put inside of the conditional statement checking if the tags were already applied. This would cause the icons to sometimes not be applied if another module had already applied tags to the player. A fix has been published to the module and this post has been edited to rectify the mistake. Thanks to @Steam_Driven for helping figure out the issue.


Great tutorial, thanks!! I’ve always tried to add in my own elements but was only restricted to text, now I can add icons!


Wow… thats actually really cool!


This will be extremely useful when you need point out things like game moderators instead of using the standard text tags or chat colors. I have actually always wondered if this was possible but never got around to trying it out myself. Nice work! I’ll definitely be trying this out.

1 Like

i remember back in 2012 where custom icons were able to be added to the playerlist… this is JUST as cool!!!

Good job on this. I ended up doing something similar with mine from the original post. Excellent work compiling this thread.


Woah, this is going to be great - about to go and add it to my game now :pray:
This looks pretty cool.

1 Like

There’s a problem, isn’t there someway to set it by grabbing the decal? Because subtracting the number takes longer since Roblox is trying to patch it cause users are stealing other user’s clothing.

Decal IDs are different from image IDs. You can get the image ID by finding the image you want in the toolbox, and then right-clicking on it and choosing the “copy image URL” option. Subtracting numbers from the decal ID until you find the image ID isn’t really feasible anymore.


I found two other ways of doing this easily with an extension, and another way by doing it VIA studio:

  1. BTRoblox: Find it VIA Chrome store, you can see it here > Find the decal, click from your images on the create page to be directed to the decal page > You’ll see an image button on the top right area (as seen here) > Click the image button, it will take you directly to the page where your image is (as seen here)

  2. Studio: Workspace > + > Decal > Properties > Paste in your decal ID > Wait a second, you’ll notice the ID changes > Your ID will then be converted to the image asset ID, just copy it from the decal properties and you’ll find it. Gyazo Preview

I personally use BTRoblox for multiple things, it has many benefits including being able to view model sources from the site including scripts.

The second one is practically pointless, it’s much easier doing it the way mentioned by ChipioIndustries.


Love this! However I am having a problem. The image does not appear in chat and I am getting this error in output;

19:39:27.941 - Mesh Manager: http request failed, contentid: 'rbxassetid://4342232872', exception: HTTP 403 (Forbidden)

I don’t know why, does somebody know how I can fix this?


just read the discussion, I was using the decal id, not image.

How did u set it in the ExtraDataInitializer?

I got it working. I don’t really understand the question, but it was because I was using the decal id, not the image id

Alright, I mean’t that too my bad.