How can I make a typewriter effect with RichText that has different colors?

Here is my typewriter function and the text I want to display, at the moment, it says the whole string and then once the typewriter effect is completed it has color.

function TypeEffect(String,Label: TextLabel)
	local charCount = #String
	
	for i=1,charCount,1 do
		Label.Text = string.sub(String,1,i)
		script.Talk:Play()
		task.wait(0.04)
	end
end

FirstText = 'Hello new <font color="#1E90FF">Fisher! </font>How can I help you?',

image
image

I don’t know if this is possible but the idea is

  • Detecting the < character and skip those texts inside until it finds >
  • Or you could do "Hello new " first and replace it with "Hello new <font color="#1E90FF">Fisher! </font>" later when the sentence is finished (without typing effect). Then continue the typing effect with "How can I help you?"

The way I went about it was each time a character was written, it got assigned as text in the label as a new string, as opposed to adding to existing characters.

--[[=[
	@voozy_v | 2025 Mar 10
]=]]

local Debris = game:GetService("Debris")

local function FormatRichText(text:string)
	local segments = {}
	local i = 1
	local inTag = false
	local currentTag = ""
	local visibleText = ""
	local tagStack = {}

	while i <= #text do
		local char = text:sub(i, i)
		if char == "<" then
			local endPos = text:find(">", i)
			if endPos then
				local tag = text:sub(i, endPos)
				table.insert(segments, { text = tag, isTag = true })

				if tag:sub(2, 2) ~= "/" then
					table.insert(tagStack, tag)
				else
					table.remove(tagStack) -- Close last tag
				end

				i = endPos + 1
			else
				i += 1
			end
		else
			table.insert(segments, { text = char, isTag = false, tags = table.clone(tagStack) })
			i += 1
		end
	end

	return segments
end

local function Construct(segments:{}, count:number)
	local built = ""
	local typed = 0

	for _, seg in ipairs(segments) do
		if seg.isTag then
			built ..= seg.text
		elseif typed < count then
			local open = ""
			local close = ""
			for _, tag in ipairs(seg.tags or {}) do
				open ..= tag
				local tagName = tag:match("<(%w+)")
				close = ("</%s>"):format(tagName) .. close
			end
			built ..= open .. seg.text .. close
			typed += 1
		end
	end

	return built
end

return function(label:TextLabel|TextBox, text:string, interval:number?)
	local existingText = label.Text ~= "" and (label.Text .. " ") or ""
	local segments = FormatRichText(text)

	local visibleCount = 0
	for _, seg in ipairs(segments) do
		if not seg.isTag then
			visibleCount += 1
		end
	end

	for i = 1, visibleCount do
		label.Text = existingText .. Construct(segments, i)
		
		local sound = script.Parent.Sound:Clone()
		sound.Parent = workspace
		sound:Play()
		Debris:AddItem(sound, 1)
		
		task.wait(interval or 0.025)
	end
end

I wrote this as part of my Label module, and this is the Write module in it with the logic. The solution is a bit hacky but it works.

This is the whole Label module if you want to use:
Label_.rbxm (17.2 KB)

There’s a disabled LocalScript named “UsageExample” in there, just enable it and put the example Dialog gui in StarterGui for it to work.

You can also choose between Wait mode and Input mode, allowing you to click to play the next text or just wait. Go to the Type module and change the delay_mode to either one. It’s currently set to Input, and the input_character is MB1 so you have to click to continue the text.

Type.config = {
	type_character_interval = 0.025,
	delay_mode = "Input", -- "Wait",
	input_character = Enum.UserInputType.MouseButton1 -- Enum.KeyCode.E
}
1 Like

after some adjusting/implementation into my current system it works perfectly, thanks!

You really shouldn’t do this. MaxVisibleGraphemes exists for exactly your use case.

2 Likes

Yeah this method is much easier

Also, for people in the future who may be viewing this post, this function I created based on what BackspaceRGB said works perfectly and is small.

The TypeEffect Function

function TypeEffect(String,Label: TextLabel)
	local StrippedCharCount = string.gsub(String,"<[^>]->", "")
	
	Label.Text = String

	for i=1,#StrippedCharCount,1 do
		Label.MaxVisibleGraphemes = i
		script.Talk:Play()
		task.wait(0.04)
	end
end

How it works:
“StrippedCharCount” removes all the rich text features from the string, which ensure that the for loop is only counting the actual letters. Which means “Hello new </font color=”#1E90FF">Fisher! How can I help you?" will turn into “Hello new Fisher! how May I help you?”. In my case, this prevents the sound from playing for rich text letters.

You then set the text to your desired string, go through the StrippedCharCount and set the MaxVisibleGraphemes to the index. Since the text is already preloaded, it includes colors and prevents the rich text features from showing like in this image

image

Instead, it shows like this VVV

image

Hope this helps!

7 Likes

Pro tip: TextLabel.ContentText is a copy of the raw text with rich-text features removed. You can use this over manually stripping the features

2 Likes