OpenTextChatService is a client sided open source implementation of the default Roblox text chat using the TextChatService API. Switching from TextChatService to OpenTextChatService is seamless, enabling endless customizability. OpenTextChatService operates on top of TextChatService.
Why OpenTextChatService?
- OpenTextChatService provides a way to add additional functionality to text chat since developers must migrate to TextChatService.
- OpenTextChatService supports all features of the current TextChatService (Channel tabs, UI Gradients, Auto complete, Chat decoration, Message callbacks, etc) while allowing you to add your own functionality.
- OpenTextChatService is more performant than TextChatService.
- OpenTextChatService addresses several bugs and strange behaviours in TextChatService.
OpenTextChatService behaves near identically to TextChatService by using "virtual
classes". Virtual classes create an interface between OpenTextChatService
and the real TextChatService.
Out of The Box
While the main intent of OpenTextChatService is to give developers a starting point to create modified Core GUI-like text chats, OpenTextChatService has with some basic customizability out of the box with TextChatConfiguration. OpenTextChatService simply acts as an interface for TextChatService, so in addition to the following Config properties, any properties specified in ChatWindowConfiguration, ChatInputBarConfiguration, ChannelTabsConfiguration, and BubbleChatConfiguration are also respected.
- MaxMessages - Maximum number of messages displayed in a given text channel
- BackgroundFadeOutTime - Time until chat window background fades out
- TextFadeOutTime - Time until chat window text fades out
- OptionToViewUntranslatedMessage - Option to show/hide untranslated messages
- FilterSpecialCharacters - Filters verified icon, premium icon and Robux icon
- OpenTextChatEnabled - Use OpenTextChatService implementation of text chat at runtime
- OpenBubbleChatEnabled - Use OpenTextChatService implementation of bubble chat at runtime
These properties can be changed by scrolling down to the attributes section of the TextChatConfiguration instance under the OpenTextChatService module. Some of these attributes can also be changed at runtime by accessing TextChatServie.TextChatConfiguration
thanks to virtual classes. An example below will go more in depth about what virtual classes are and what behaviour they enable. While these configurations might be all you need, OpenTextChatService is intended to be modified so you can add more complex behaviour like:
- Chat typing indicators
- Chat cooldowns
- An emoji selector
- Profile popups
- Replies, reactions, etc
- Modified chat bubbles
- Chat resizing
OpenTextChatService simply acts as an interface, it is up to you as the developer
to add your own functionality.
OpenTextChatEnabled and OpenBubbleChatEnabled decide what OpenTextChatService
will handle. If you value the chat reporting feature that is only available
through Roblox's text chat, but would still like to customize bubble chats
using OpenTextChatService, you could set OpenTextChatEnabled to false
and leave OpenBubbleChatEnabled set to true
Usage
local PlayerScripts = game.Players.LocalPlayer.PlayerScripts -- Or wherever
local TextChatService = require(PlayerScripts.OpenTextChatService)
-- OpenTextChatService is now initialized
- Replace all CLIENT instances of
game:GetService("TextChatService")
withrequire(pathToOpenTextChatService)
- Ensure that at least one local script requires OpenTextChatService at run time to initialize the service
And thats all! OpenTextChatService will behave almost exactly as TextChatService did with the exception of being handled and rendered by the the developer. Should you run into any errors that are obfuscated as a result of OpenTextChatService’s virtual classes, set the module attribute Enabled
to false
and you will be able to go back to interfacing with the regular TextChatService. Enabled cannot be changed during run time, i.e, you cannot switch between TextChatService and OpenTextChatService during runtime.
You should always access TextChatService by requiring the OpenTextChatService
module on the client. Accessing TextChatService via game:GetService will
bypass OpenTextChatService overrides defined in virtual classes.
Gallery
Virtual Classes
So what exactly are “virtual classes”? Virtual classes act as an intermediary layer between the client programmer and an instance that is being “virtualized”. They allow us to define unique behaviour like custom methods, properties and events. For general purposes, it is not necessary to understand how to implement a virtual class, but it is important to understand that TextChatService and most of its decedents (ChatInputBarConfiguration, ChatWindowConfiguration, BubbleChatConfiguration, ChannelTabsConfiguration, TextChatConfiguration) are implemented this way, and as a result may obfuscate errors. We must also consider the following:
- Implementing these instances as virtual classes means that we must access TextChatService via
require(pathToOpenTextChatService)
instead ofgame:GetService("TextChatService")
or else we bypass the abstraction layer. - You cannot access virtual classes by navigating the Roblox instance tree. This is why you cannot expect game.TextChatService or TextChannels.Parent to return a virtual class since you have navigated from the game instance tree.
- You cannot directly parent instances to a virtual class such as BubbleChatConfiguration. The example below shows how to do so during runtime using the unary operator.
The benefit of Virtual Classes is that OpenTextChatService is drag and drop. You don’t do anything besides switch game:GetService("TextChatService")
to require(pathToOpenTextChatService)
.
Key takeaway: Virtual classes allow for custom properties, methods and events.
You must access TextChatService via require(pathToOpenTextChatService) as a
result.
Resolve Operator
While virtual classes give us the ability to modify instance behaviour, they themselves are not instances. This makes operations on the Roblox instance tree less trivial. For the most part, you can assume that only TextChatService, ChatInputBarConfiguration, ChatWindowConfiguration, BubbleChatConfiguration, ChannelTabsConfiguration and TextChatConfiguration are implemented as virtual classes (in reality, all decedents of TextChatService are implemented as virtual classes, but most resolve to the instance they virtualize automatically for convenience). Therefore, if you wish to use these instances as arguments where Roblox expects a real instance, you must manually resolve them.
local gradient = Instance.new("UIGradient")
gradient.Parent = TextChatService.TextChannels -- Ok, automatically resolves
gradient.Parent = TextChatService.BubbleChatConfiguration -- Error
gradient.Parent = -TextChatService.BubbleChatConfiguration -- Ok
gradient.Parent = TextChatService.BubbleChatConfiguration:GetInstance() -- Ok
-- TextChatService, ChatInputBarConfiguration, ChatWindowConfiguration,
-- BubbleChatConfiguration, ChannelTabsConfiguration and
-- TextChatConfiguration do not automatically resolve
More than likely, this will not concern you unless you need to parent an instance to BubbleChatConfiguration during runtime.
TLDR: All decedents of TextChatService are implemented as virtual classes. All
instances except for TextChatService, ChatInputBarConfiguration,
ChatWindowConfiguration, BubbleChatConfiguration, ChannelTabsConfiguration.
and TextChatConfiguration will automatically resolve.
Example
Since its rather hard to explain all this “virtual class”, “overriding properties” and “resolving” nonsense, here is an example to illustrate how you can use these new properties with OpenTextChatService.
local PlayerScripts = game.Players.LocalPlayer.PlayerScripts
local TextChatService = require(PlayerScripts.OpenTextChatService)
-- *
-- The TextChatConfiguration defined under the OpenTextChatService
-- module is accessible via TextChatService at run time. It is implemented
-- such that you can directly access attributes using the index operator!
-- *
TextChatService.TextChatConfiguration.MaxMessages = 100
TextChatService.TextChatConfiguration.BackgroundFadeOutTime = 10
TextChatService.TextChatConfiguration.TextFadeOutTime = math.huge
TextChatService.TextChatConfiguration.OptionToViewUntranslatedMessage = false
-- *
-- The following lines will produce an error and or not be respected as
-- indicated in "Usage" or "Out Of The Box". These properties cannot be
-- changed during runtime.
-- *
TextChatService.TextChatConfiguration.OpenTextChatEnabled = false -- Errors
TextChatService.TextChatConfiguration.OpenBubbleChatEnabled = false -- Errors
PlayerScripts.OpenTextChatService:SetAttribute("Enabled", false)
-- *
-- The following properties and methods (and more), are available thanks
-- to virtual classes. Read more in the additions section.
-- *
TextChatService:GetPropertyChangedSignal("ChatActive"):Connect(function()
print(TextChatService.ChatActive)
end)
print(TextChatService.ChatAbsoluteSize)
print(TextChatService.ChatAbsolutePosition)
TextChatService:OpenChat()
TextChatService:CloseChat()
-- *
-- Be mindful of when you are in the real instance tree
-- *
local a = TextChatService.TextChannels.Parent.TextChannels
local b = TextChatService.TextChannels
print(a == b) -- "True"
print(a) -- "TextChannels"
print(b) -- "TextChannels"
local c = TextChatService.BubbleChatConfiguration.Parent.BubbleChatConfiguration
local d = TextChatService.BubbleChatConfiguration
local e = TextChatService.TextChannels.Parent.BubbleChatConfiguration
print(c == d) -- "True"
print(c == e) -- "False"
print(c) -- "BubbleChatConfiguration (Virtual)"
print(d) -- "BubbleChatConfiguration (Virtual)"
print(e) -- "BubbleChatConfiguration"
-- *
-- If you wish to parent an instance to a virtual class or resolve the instance
-- that is being virtualized, use the resolve operator. It will work
-- on any virtual class (i.e. ChatInputBarConfiguration, ChatWindowConfiguration,
-- BubbleChatConfiguration, ChannelTabsConfiguration, TextChatConfiguration)
-- All other decedents of TextChatService are automatically resolved for convenience.
-- Using unary "-" and ":GetInstance()" are both valid sytntax
-- *
local gradient = Instance.new("UIGradient")
gradient.Parent = TextChatService.TextChannels -- Ok
gradient.Parent = TextChatService.BubbleChatConfiguration -- Errors
gradient.Parent = TextChatService.BubbleChatConfiguration:GetInstance() -- Ok
gradient.Parent = -TextChatService.BubbleChatConfiguration -- Ok
gradient.Parent = (-TextChatService).BubbleChatConfiguration -- Ok
-- *
-- The only time you should be using the resolve operator is when you need
-- to work directly with the Roblox instance tree and or provide a real instance
-- as a parameter. Otherwise you will bypass the abstraction layer or
-- unintentionally try to apply the resolve operator to other types.
-- *
print(TextChatService.Name) -- Ok
print(TextChatService.TextChannels) -- Ok
print(TextChatService.ChatActive) -- Ok
print(TextChatService:GetInstance().Name) -- Avoid
print(TextChatService:GetInstance().TextChannels) -- Avoid
print(TextChatService:GetInstance().ChatActive) -- Errors
local object = Instance.new("ObjectValue")
object.Value = -TextChatService -- Ok
-- *
-- If you wish to use the unary "-" opperator to resolve insatnces, the following
-- line pairs are equivilent to illustate why you may be running into an error.
-- *
print(-TextChatService.Name) -- Errors
print(TextChatService.Name:GetInstance()) -- Errors
print((-TextChatService).Name) -- Ok
print(TextChatService:GetInstance().Name) -- Ok
-- *
-- TextChatService, ChatInputBarConfiguration, ChatWindowConfiguration,
-- BubbleChatConfiguration, ChannelTabsConfiguration and TextChatConfiguration
-- are the only instances that are not automatically resolved. If you are unsure
-- you can check by print debugging.
-- *
print(TextChatService) -- "TextChatService (Virtual)"
print(TextChatService:GetInstance()) -- "TextChatService"
print(TextChatService.TextChannels) -- "TextChannels"
print(TextChatService.TextChannels:GetInstance()) -- Errors
-- *
-- The resolve operator is something that you will likely never have to touch
-- unless you have to modify add children to BubbleChatConfiguration at run time.
-- In which case the following will work.
-- *
local image = Instance.new("ImageLabel")
image.Parent = TextChatService.BubbleChatConfiguration:GetInstance() -- Ok
image.Parent = TextChatService:GetInstance().BubbleChatConfiguration -- Ok
image.Parent = -TextChatService.BubbleChatConfiguration -- Ok
image.Parent = (-TextChatService).BubbleChatConfiguration -- Ok
Additions
OpenTextChatService provides additional properties, methods and events for quality of life (yay virtual classes).
For more info on Additions & Overrides refer to the virtual class documentation.
-
TextChatService
- ChatActive - Whether or not the chat is toggled on or off. You can determine if the entire chat window is visible or not. Equivalent to ExperienceChat.Enabled.
- ChatAbsoluteSize - You can now determine the TOTAL size of the chat window. Equivalent to ExperienceChat.applayout.layout.AbsoluteContentSize. Not to be confused with TextChatService.ChatWindowConfiguration.AbsoluteSize.
- ChatAbsolutePosition - Equal to the absolute position of the chat window. Equivalent to ExperienceChat.applayout.AbsolutePosition.
- ChatBackgroundFaded - Whether or not the background of the text chat is in the faded out state.
- ChatBackgroundTransparency - The transparency mask that is currently applied to chat background. Between 0 (none) and 1 (transparent). Represents how faded out the background is.
- ChatTextFaded - Whether or not the messages in the text chat are in the faded out state.
- ChatTextTransparency - The transparency mask that is currently applied to chat text. Between 0 (none) and 1 (transparent). Represents how faded out the chat messages are.
- OpenChat - Method to force open the chat
- CloseChat - Method to force close the chat
-
TextChatConfiguration
- MaxMessages - The maximum number of messages displayed in any given text channel
- BackgroundFadeOutTime - The time it takes for the chat background elements to fade out when no new activity is detected.
- TextFadeOutTime - The time it takes for the chat text to fade out when no new activity is detected. Will be clamped if less than than BackgroundFadeOutTime.
- OptionToViewUntranslatedMessage - Allows a user to see the untranslated version of a message when set to true and TextChatService.ChatTranslationEnabled is also true.
- FilterSpecialCharacters - Determines whether special characters are filtered (verified icon, premium icon and Robux icon).
- OpenTextChatEnabled - Determines whether to use the OpenTextChatService implementation of text chat at runtime or TextChatService (Not the same as ChatWindowConfiguration.Enabled, etc). Cannot be changed during runtime.
- OpenBubbleChatEnabled - Determines whether to use the OpenTextChatService implementation of bubble chat at runtime or TextChatService, this includes :DisplayBubble. (Not the same as BubbleChatConfiguration.Enabled). Cannot be changed during runtime.
Fixes
OpenTextChatService has fixed many issues present in the default TextChatService implementation. The list below highlights some of these issues, but is not exhaustive.
- Text Stroke Not Fading
- Special Characters Not Filtering (Verified Icon, etc)
- Chat Window Flashing
- TextChatCommand Autocomplete Not Working
- Odd padding layout order
- and more!
Performance
OpenTextChatService is significantly faster than TextChatService (as much as a 40x improvement when receiving a single message). Mass message spamming is challenging for both implementations, but OpenTextChatService is also around 3x faster. However, autocomplete can be slower on the first query (not noticeably). It has and will been improved upon in the future.
Micro Profiler
* Result includes ExperienceChatMain overhead
In addition to these optimizations, OpenTextChatSerivce disables the default chat window eliminating most CoreScript/ExperiencesChatMain performance issues.
Installation & Demo
To install, first get the model from the creator store.
Note: OpenTextChatService will receive updates to maintain parity with TextChatService so your fork may become out of date.
- Insert the OpenTextChatService module into
StarterPlayer.StarterPlayerScripts
or your location of preference. - Follow the usage guide for implementation.
The demo above provides a side by side comparison of OpenTextChatService (left) and TextChatService (right). The demo also includes an example use case. Try opening the a player inspect menu by clicking on a players username in the chat window.
Change Log
April 15, 2025 1.0.0 - Release
- OpenTextChatService released
April 15, 2025 1.0.1 - Minor Fixes
- Up/Down arrow autocomplete navigation fixed
- Added VirtualClass:GetInstance()
April 15, 2025 1.0.2 - Minor Fixes
- Deprecated TextChatService.ChatVisible in favour of TextChatService.ChatActive
Upcoming Changes
- Optimized calls to TweenService.SmoothDamp
- Optimizations to AutoComplete
- Release resource as a package
- Code annotations
- Exposed CoreGuiChatConnections
Limitations & Considerations
OpenTextChatService is fairly robust and mostly faithful to Roblox’s TextChatService implementation but has some notable limitations and differences.
-
Parity Issues
- Transparency property of chat message UIGradient is not supported.
- Bubble offset and speed may not be the exact same.
- Emoji autocomplete uses different phrases/keywords.
- Channel tab layout order and size may differ.
- Bubble chat ellipsis styling follows BubbleChatConfiguration rather than being static.
- Translation icon sizing and alignment is slightly different.
- Higher resolution images are used for the chat window.
- “Option to View Untranslated Message” option in the escape menu is not respected since there is no API to determine that value. You can add your own user controlled implementation through TextChatConfiguration.OptionToViewUntranslatedMessages.
- The report button is absent since there is no API to open that menu.
-
Limitations
- Voice chat bubbles will not appear above chats.
- You cannot bind to chat window events via SetCore while using OpenTextChatService.
-
Considerations
- Virtual classes may have undiscovered edge cases.
- Console implementation is not yet tested.
Some parity issues may be addressed in the future, others are for your discretion.
OpenTextChatService was developed to address TextChatService's lack of
customizability. It was also developed with my own use case in mind, hence
some of the limitations highlighted above. However, I have done my best to make
it easy to integrate into any experience. This resource is simply a product
of my frustrations, intended to help anyone else who has similar issues.
Virtual Classes Documentation
About Virtual Classes (Advanced Developers)
Some of the information below may be out of date as the implementation has undergone several revisions.
OpenTextChatService is a “virtual class” that mirrors behaviour of TextChatService. In fact, most of the time, OpenTextChatService simply acts as a proxy for TextChatService. This allows for:
- Seamless switching between TextChatService and OpenTextChatService, just replace
game:GetService("TextChatService")
withrequire(pathToOpenTextChatService)
. OpenTextChatService will behave as you would expect TextChatService to. - Respect for property changes in TextChatService during runtime
- Overriding properties and methods of classes for proper functionality
- Additional functionality with custom properties and methods
Most descendants of TextChatService are implemented this way.
Additions & Overrides
Given that OpenTextChatService disables ChatWindowConfiguration, ChatInputBarConfiguration, ChannelTabsConfiguration and BubbleChatConfiguration at run time, properties such as TextChatService.ChatWindowConfiguration.Enabled are “virtualized” or “overridden” to mimic proper behaviour. This goes for other properties as well. This list is not comprehensive. If you want to truly understand the behaviour, you will have to look through the source yourself.
TextChatService
Property Addition: ChatActive
-- Equivalent to the ExperienceChat.Enabled
-- i.e, Whether the chat is visible
Deprecated Property Addition: ChatVisible [readonly]
-- Unreliable, deprecated in favour of ChatActive
Property Addition: ChatAbsoluteSize [readonly]
-- Equivalent to the ExperienceChat.applayout.layout.AbsoluteContentSize
-- i.e, The total size of the chat window, not to be confused
-- with ChatWindowConfiguration.AbsoluteSize
Property Addition: ChatAbsolutePosition [readonly]
-- Equivalent to the ExperienceChat.applayout.AbsolutePosition
-- i.e, The top right corner of the chat window
Property Addition: ChatBackgroundFaded [readonly]
-- Whether or not the chat background has faded out
Property Addition: ChatBackgroundTransparency [readonly]
-- Extent of the fading applied to chat background
Property Addition: ChatTextFaded [readonly]
-- Whether or not chat messages have faded out
Property Addition: ChatTextTransparency [readonly]
-- Extent of the fading applied to chat messages
Method Addition: OpenChat [yields]
-- Force open the chat window
Method Addition: CloseChat [yields]
-- Force close the chat window
ChatWindowConfiguration
Property Override: AbsoluteSize [readonly]
-- Equivalent to the ExperienceChat.applayout.AbsoluteSize
-- i.e, The size of the chat window
Property Override: AbsolutePosition [readonly]
-- Equivalent to the ExperienceChat.applayout.AbsolutePosition
-- i.e, The size of the chat window
Property Override: Enabled
-- Determines whether the chat window is visible
-- Initialized to TextChatService.ChatWindowConfiguration.Enabled
ChatInputBarConfiguration
Property Override: AbsoluteSize [readonly]
-- Equivalent to the ExperienceChat.applayout.chatInputBar.AbsoluteSize
-- i.e, The size of the chat input bar
Property Override: AbsolutePosition [readonly]
-- Equivalent to the ExperienceChat.applayout.chatInputBar.AbsolutePosition
-- i.e, The position of the chat input bar
Property Override: IsFocused [readonly]
-- Whether or not the chat input bar is focused
Property Override: TextBox
-- Proxy
Property Override: Enabled
-- Determines whether the chat input bar is visible
-- Initialized to TextChatService.ChatInputBarConfiguration.Enabled
ChannelTabsConfiguration
Property Override: AbsoluteSize [readonly]
-- Equivalent to the ExperienceChat.applayout.channelBar.AbsoluteSize
-- i.e, The size of the channel bar
Property Override: AbsolutePosition [readonly]
-- Equivalent to the ExperienceChat.applayout.channelBar.AbsolutePosition
-- i.e, The position of the channel window
Property Override: Enabled
-- Determines whether the chat window is visible
-- Initialized to TextChatService.ChannelTabsConfiguration.Enabled
BubbleChatConfiguration
Property Override: MaxBubbles
-- Maximum bubble count
-- Initialized to TextChatService.BubbleChatConfiguration.MaxBubbles
-- Forced to zero
Property Override: Enabled
-- Determines whether bubble chat is enabled
-- Initialized to TextChatService.BubbleChatConfiguration.Enabled
TextChatConfiguration
Calls are equivalent to the using GetAttribute and SetAttribute on the TextChatConfiguration
Property Addition: BackgroundFadeOutTime
-- Time until background fades out
Property Addition: TextFadeOutTime
-- Time until text fades out
Property Addition: MaxMessages
-- Max messages in each channel
Property Addition: OpenBubbleChatEnabled [readonly]
-- Uses OpenTextChatService implementation when true
Property Addition: OpenTextChatEnabled [readonly]
-- Uses OpenTextChatService implementation when true
Property Addition: OptionToViewUntranslatedMessage
-- Adds/remove the show/hide translation button
Property Addition: FilterSpecialCharacters
-- Filters special characters such as the verified icon when set to true
Implementation
Virtual classes are designed to help create parity between regular TextChatService
and OpenTextChatService. They are not *intended* to be tampered with and are not
user friendly.
Adding overrides and additions to virtual classes is not trivial, and is therefore only recommended if absolutely necessary. Necessary overrides are already implemented. More than likely, any modifications you want to make to OpenTextChatService will not require tampering with virtual classes. To be honest, I’m not really sure why I’m documenting this but I’m sure someone will want to know how they work.
- Any virtual class you create will (or at least SHOULD) behave the exact same as its “mirror” or the instance you are “virtualizing” if you do not specify any overrides.
- Roblox will not understand if you pass a virtual class as an argument into a function that expects the actual class (seriously, if you have somehow got his far virtual classes aren’t really designed for whatever you are doing, they are good great for what I did and thats about it)
The following example shows how to create a virtual class for a Part instance and override the Name property. Indexing any property besides Name will act upon the Part.
-- Creates a virtual class of a Part instance
local Part = Instance.new("Part")
VirtualPart = module.VirtualClass.new(Part)
VirtualPart:__addgetter("Name", function(self, key) return self.__class[key] end)
-- Initialize name to the mirror's name
VirtualPart.__class.Name = Part.Name
VirtualPart:__addsetter("Name", function(self, key, value) self.__class[key] = value end)
-- Add property changed signal
VirtualPart:__addsignal("Name")
VirtualPart.Name = "Hello"
VirtualPart.Color3 = Color3.fromRGB(255, 0, 0)
print(VirtualPart.Name) -- "Hello"
print(VirtualPart.Color3) -- "255, 0, 0"
print(Part.Name) -- "Part"
print(Part.Color3) -- "255, 0, 0"
print(VirtualPart) -- "Part (Virtual)"
print(Part) -- "Part"
print(-VirtualPart) -- "Part"
print(VirtualPart == Part) -- "false"
print(-VirtualPart == Part) -- "true"
Tags: custom textchatservice open source client legacy chat modify fork textchat service text chat reply reaction cooldown typing indicator chat active visibility visible absolutesize size emoji selector command autocomplete legacy chat alternative gradient chat tags chat wrapper rewrite chat background visible / faded fade out chat resize chat transparency
Made with by TOXIC