Quick answer: The most common cause is that the effect is not registered with the RichTextLabel node. You must add your RichTextEffect instance to the custom_effects array on the RichTextLabel via the inspector or code.
Here is how to fix Godot richtext effect custom not working. You created a custom RichTextEffect class, added your BBCode tag to the text, and nothing happens. The text renders as plain text with the tag visible, or the tag disappears but _process_custom_fx is never called. Custom BBCode effects in Godot 4 require several pieces to line up correctly, and missing any one of them causes silent failure.
How Custom RichTextEffects Work in Godot 4
A custom RichTextEffect is a GDScript or C# class that extends RichTextEffect. It defines a bbcode property that determines the tag name and a _process_custom_fx method that runs per character per frame. For the effect to work, three things must be true simultaneously: the effect must be registered with the RichTextLabel, BBCode must be enabled, and the tag in the text must exactly match the bbcode property.
Here is a minimal working example of a shake effect:
# shake_effect.gd
@tool
class_name ShakeEffect
extends RichTextEffect
var bbcode := "shake"
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
var freq := char_fx.env.get("freq", "4.0").to_float()
var amp := char_fx.env.get("amp", "3.0").to_float()
var offset := Vector2(
randf_range(-amp, amp),
randf_range(-amp, amp))
char_fx.offset += offset
return true
And the RichTextLabel BBCode text would be:
This text is normal. [shake freq=6.0 amp=2.0]This text shakes![/shake]
Fix 1: Register the Effect with the RichTextLabel
The most common reason a custom effect does not work is that it was never added to the RichTextLabel’s custom_effects array. Creating the script alone is not enough—you must explicitly install it.
Via the Inspector: Select your RichTextLabel node, find the custom_effects property under the BBCode section, click the array to expand it, and add a new element. Drag your effect script or create a new instance of your effect class.
Via code:
extends Control
@onready var label := $RichTextLabel
func _ready() -> void:
# Create and register the effect
var shake := ShakeEffect.new()
label.install_effect(shake)
# Now set the text with BBCode
label.text = "Normal text. [shake]Shaking text![/shake]"
You can also use label.custom_effects.append(shake), but install_effect() is the preferred API as it handles internal registration properly.
Fix 2: Enable BBCode on the RichTextLabel
Custom effects only work when bbcode_enabled is true. If BBCode is disabled, the RichTextLabel renders text as plain text and ignores all tags, including custom ones.
func _ready() -> void:
var label := $RichTextLabel
# This is required for custom effects
label.bbcode_enabled = true
label.text = "[shake]This only works with bbcode_enabled = true[/shake]"
In the inspector, this is the BBCode > Enabled checkbox. It defaults to false on new RichTextLabel nodes, which trips up developers who expect it to be on by default.
Fix 3: Match the bbcode Property to the Tag Name
The bbcode variable in your RichTextEffect class must exactly match the tag you use in the BBCode text. This is case-sensitive and must not include brackets.
# WRONG: bbcode includes brackets
var bbcode := "[wobble]" # Will NOT match [wobble]...[/wobble]
# WRONG: bbcode has extra spaces
var bbcode := " shake " # Will NOT match [shake]...[/shake]
# CORRECT: plain tag name, no brackets, no spaces
var bbcode := "wobble" # Matches [wobble]...[/wobble]
If you rename your tag, update both the bbcode property in the script and every occurrence in your RichTextLabel text. A mismatch will cause the tag to either render as literal text or be stripped with no effect applied.
Fix 4: Add the @tool Annotation
If you want to see your custom effect in the editor (not just at runtime), your script must have the @tool annotation at the top. Without @tool, the effect only runs during gameplay.
@tool # Required for editor preview
class_name WaveEffect
extends RichTextEffect
var bbcode := "wave"
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
var speed := char_fx.env.get("speed", "5.0").to_float()
var height := char_fx.env.get("height", "4.0").to_float()
char_fx.offset.y += sin(
char_fx.elapsed_time * speed + char_fx.relative_index * 0.5
) * height
return true
Note that @tool makes the entire script run in the editor. If your effect script has side effects in _ready or other lifecycle methods, guard them with if Engine.is_editor_hint(): return to prevent unexpected behavior.
Fix 5: Return true from _process_custom_fx
The _process_custom_fx method must return true to indicate that the character should be rendered. If you forget the return statement (which defaults to returning null / falsy) or explicitly return false, the characters inside your tag will become invisible.
# BAD: missing return — characters disappear
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
char_fx.offset.y += 5.0
# No return statement = returns null = characters hidden
# GOOD: explicit return true
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
char_fx.offset.y += 5.0
return true
Returning false is only useful if you intentionally want to hide specific characters as part of a typewriter or reveal effect.
Working with Effect Parameters
Custom BBCode tags can accept parameters using the [tag key=value] syntax. All parameter values arrive as strings in the char_fx.env dictionary and must be converted manually:
@tool
class_name ColorPulseEffect
extends RichTextEffect
var bbcode := "pulse"
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
var speed := char_fx.env.get("speed", "3.0").to_float()
var color_str := char_fx.env.get("color", "ff0000")
var pulse_color := Color(color_str)
var t := abs(sin(char_fx.elapsed_time * speed))
char_fx.color = char_fx.color.lerp(pulse_color, t)
return true
# Usage: [pulse speed=2.0 color=ff6600]Pulsing orange text[/pulse]
Always provide default values via .get(key, default) to handle cases where the user omits a parameter. This prevents crashes from missing dictionary keys.
“Custom RichTextEffects are one of Godot’s most powerful UI features, but the setup has too many silent failure modes. Check registration, check bbcode_enabled, check the return value, and check the tag name. In that order.”
Related Issues
If you are building a dialogue system with rich text effects and running into other issues, see Fix: Godot HTTPRequest Not Completing or Timing Out for fetching dialogue data from a server. For collecting UI rendering bugs from players, check out How to Add a Bug Reporter to a Godot Game.
The RichTextEffect system is elegant once it works. The debugging frustration comes from Godot not telling you what is wrong. Follow the checklist: register, enable, match, return true.