Quick answer: RichTextLabel shows [img] and [icon] tags as plain text when bbcode_enabled is false, when the image path is not a res:// URI, when the resource is atlas-only and not loaded into the project cache, or when fit_content is off and the image is clipped to zero. Enable BBCode, use absolute paths, preload textures, turn on fit_content, and use add_image for runtime graphics.
Here is how to fix Godot RichTextLabel when [img] tags refuse to render as inline images. You write a tutorial popup like "Press [img]res://ui/icon_a.png[/img] to interact" and instead of seeing the icon you see the literal string with brackets. Or worse, the tag disappears but no image appears in its place — the text just has a mysterious gap. RichTextLabel is one of the most powerful Control nodes in Godot, but it is also one of the most particular about how its inputs are configured.
The Symptom
You assign a BBCode string to a RichTextLabel and the inline image markup does not produce an image. Three flavors of failure are common:
Tags shown as raw text. The label displays [img]res://icons/sword.png[/img] exactly as you wrote it, with the brackets visible. None of the text inside the tag is hidden, and no image renders.
Silent drop. The tag is consumed by the parser — the brackets and inner text disappear — but no image appears. The surrounding text reflows as if the image was zero-width, leaving an unexpected blank.
Works for some images, not others. A standalone PNG renders fine, but tags that reference an AtlasTexture, a SubViewport texture, or a runtime-generated ImageTexture come up empty. The path looks correct in the inspector and the resource exists.
What Causes This
bbcode_enabled is false. RichTextLabel has two modes: plain text and BBCode. In plain text mode it treats every character as literal, so [img] is just six characters in a row. The flag defaults to false on freshly created nodes, and pasting a BBCode string into text in the inspector does not flip it for you.
Path is relative to the scene, not the project. RichTextLabel parses image paths through the resource loader, which expects res://-prefixed absolute paths. A path like icons/sword.png or ../assets/sword.png may have worked in your file system browser, but the loader cannot resolve it from a label running inside an arbitrary scene.
Atlas-only resource not loaded. The string-form [img]path[/img] only knows how to load Texture2D resources directly off disk. AtlasTextures live inside .tres resource files and cannot be referenced by a path that points at a region inside an atlas. Even if you give it the path to the .tres, RichTextLabel will load it as the wrapping resource, not the inner sub-region.
fit_content disabled. If fit_content is off and the label has a fixed height smaller than the image, the image is clipped or skipped depending on layout. If the height is zero, you see nothing even though the parser accepted the tag.
Resource not exported in the build. Images that are only referenced from BBCode strings — never preloaded or assigned in any scene — can be stripped from exported projects. The path string in the BBCode does not count as a project reference, so the cooker assumes the file is unused.
The Fix
Step 1: Enable BBCode. In the inspector, find the RichTextLabel section and turn on BBCode Enabled. In code, set bbcode_enabled = true before you assign the text. If you assign text first and flip the flag afterward, the existing text is re-parsed, but it is safer to enable the flag in _ready ahead of any text assignments.
# TutorialLabel.gd
extends RichTextLabel
# Preload icons so the cooker keeps them in the build
const ICON_A = preload("res://ui/icon_a.png")
const ICON_B = preload("res://ui/icon_b.png")
func _ready():
bbcode_enabled = true
fit_content = true
scroll_active = false
text = "Press [img=24]res://ui/icon_a.png[/img] to attack." + \
"\nHold [img=24]res://ui/icon_b.png[/img] to block."
Step 2: Use absolute res:// paths. Always start image paths with res://. The [img=NN] form lets you specify a width in pixels, which is useful for inline icons that need to match the line height of the surrounding text.
Step 3: Preload textures used in BBCode. The simplest way to keep an image in the export is to preload() it as a constant in any script. The exporter sees the reference, includes the file, and the runtime loader can find it when it parses the BBCode string. If you generate BBCode dynamically from data, add a single autoload script that preloads every dynamic icon path.
Step 4: Turn on fit_content. Without it the label has a fixed size and any image that does not fit gets clipped or hidden. With it the label expands to accommodate inline images and wrapping text. Pair it with scroll_active = false if you do not want a scrollbar appearing on tooltips and dialog boxes.
Step 5: Use add_image for atlas or runtime textures. When the texture is an AtlasTexture, a SubViewport image, or anything you build at runtime, do not try to inline it through a BBCode string. Use the imperative API to insert it at the current cursor position.
# Insert an AtlasTexture inline programmatically
extends RichTextLabel
@export var sword_atlas: AtlasTexture
func _ready():
bbcode_enabled = true
fit_content = true
clear()
append_text("Equipped: ")
add_image(sword_atlas, 24, 24,
Color.WHITE,
INLINE_ALIGNMENT_CENTER)
append_text(" Iron Sword")
add_image takes a Texture2D directly, so any Resource you can hold in a variable can be displayed inline. The arguments after the texture set the display width, height, modulate color, and alignment relative to the line.
Custom Inline Graphics with install_effect
For animated icons or graphics that respond to game state, register a custom RichTextEffect via install_effect and reference it through a custom BBCode tag. The effect receives a CharFXTransform on every frame and can modify its glyph’s offset, color, or visibility — useful for pulsing prompt icons or controller-button graphics that change with the active input device.
extends RichTextEffect
class_name ButtonPromptEffect
var bbcode = "prompt"
func _process_custom_fx(char_fx):
var pulse = sin(char_fx.elapsed_time * 4.0)
char_fx.color.a = 0.6 + 0.4 * abs(pulse)
return true
Install it on the label with install_effect(ButtonPromptEffect.new()) and then wrap inline icons in [prompt][img]...[/img][/prompt] for the pulsing effect.
“RichTextLabel is a tiny markup engine. Treat it like one: enable the parser, give it absolute paths, and reach for the imperative API when the markup form runs out of road.”
Related Issues
If your BBCode color and font tags work but the layout drifts, see RichTextLabel Line Height Inconsistent. If your custom RichTextEffect compiles but never runs, check RichTextEffect Not Applying.
Always preload icons referenced in BBCode strings — the exporter cannot see them otherwise.