How to make a Typewriter Effect in Unity with TextMeshPro
A typewriter reveals text one character at a time and it's one of those small details that makes a big difference in games where dialogues have a main role.
Typewriter effect final look
In Unity, the easiest way to do this with TextMeshPro (TMP) is by using its maxVisibleCharacters property.
Why use maxVisibleCharacters?
Usually the most direct way of making a typewriter is to add a new character to the text, one by one. This works fine until you start using rich text tags like <b>, <color>, or <i>.
Typewriter problem with rich text tags
As you see in the video, this method breaks any effect while the tags are not fully appended to the text! And in Text Mesh Pro, the text box gets recalculated as well (which might make your layout shift)
TMP solves this problem with the maxVisibleCharacters property. You set the full text string up front, set maxVisibleCharacters to 0, and then increment it over time. TMP handles all parsing before anything is shown, so rich text tags are never exposed to the player and don’t break.
(This is the approach we use in Text Animator for Unity under the hood as well, with the TMP integration).
Setting up the scene
Let’s implement it!
- First, create a
canvas - Next, add a GameObject as a child and attach a
TextMeshProUGUIcomponent - Also add a new script for our typewriter

The core implementation
Here's the full TypewriterEffect script:
using System.Collections;
using TMPro;
using UnityEngine;
[RequireComponent(typeof(TextMeshProUGUI))]
public class TypewriterEffect : MonoBehaviour
{
[Tooltip("Characters revealed per second")]
[SerializeField] private float charactersPerSecond = 20f;
private TextMeshProUGUI _textComponent;
private Coroutine _typingCoroutine;
private void Awake()
{
_textComponent = GetComponent<TextMeshProUGUI>();
}
/// <summary>
/// Reveals the given text with a typewriter effect.
/// Call this to start a new line of dialogue.
/// </summary>
public void ShowText(string text)
{
// Stop any existing typing before starting a new one
if (_typingCoroutine != null)
StopCoroutine(_typingCoroutine);
// set the entire text once, but make it hidden
_textComponent.text = text;
_textComponent.maxVisibleCharacters = 0;
// Force TMP to parse the text and compute character count
_textComponent.ForceMeshUpdate(); // might not be necessary in all cases
_typingCoroutine = StartCoroutine(TypeText());
}
private IEnumerator TypeText()
{
int totalCharacters = _textComponent.textInfo.characterCount;
float delay = 1f / charactersPerSecond;
for (int i = 0; i < totalCharacters; i++)
{
_textComponent.maxVisibleCharacters = i + 1;
yield return new WaitForSeconds(delay);
}
// Ensure everything is visible at the end
_textComponent.maxVisibleCharacters = totalCharacters;
_typingCoroutine = null;
}
/// <summary>
/// Immediately shows all text (useful for a "skip" input).
/// </summary>
public void SkipToEnd()
{
if (_typingCoroutine != null)
{
StopCoroutine(_typingCoroutine);
_typingCoroutine = null;
}
_textComponent.ForceMeshUpdate();
_textComponent.maxVisibleCharacters = _textComponent.textInfo.characterCount;
}
public bool IsTyping => _typingCoroutine != null;
}A few things worth noting:
ForceMeshUpdate()tells TMP to parse the text immediately, before the coroutine starts reading characterCount. Without this call, textInfo.characterCount may return 0 on the first frame. Calling it once before the loop is all you need. It might not be needed in all cases (e.g. if you wait some frames before showing the text), but we have found it to be more stable to be included at the beginning anyways. textInfo.characterCount counts only visible characters, not the raw string length. This means rich text tags like <color=#ff0000>red text</color> are not counted, and the effect stops exactly when the last real character is shown.Finally, to use our typewriter we can call ShowText
typewriter.ShowText("I am <b>very</b> tired of this <color=#ff4444>dungeon</color>.");What’s next?
While this implementation works well for the basics, building out critical features like per-character pauses, dialogue integration, and sound (which we'll cover in a follow-up post) can require a lot of time.
We built Text Animator for Unity for handling all the heavy lifting for you! It handles all of these complexities out of the box bringing typewriter effects, per-character animations, inline effects, and rich text extensions directly to your TMP object with a simple drop-in component.
Adding the Text Animator component
Beyond the typewriter, Text Animator adds its own tag syntax for per-character effects that TMP doesn't have natively:
"I am <shake>very</shake> tired of this <color=#ff4444>dungeon</color>."
That <shake> tag makes each character in "very" wobble independently, no extra code, and of course you have many other typewriter features ready for you, out of the box.



Some of the Text Animator built-in effects
Anyways, back to the script!
Putting it together in a dialogue system
The TypewriterEffect we wrote earlier component works as a building block. A basic dialogue manager would:
- Hold a queue of dialogue lines
- Call
typewriter.ShowText(nextLine)when advancing - On player input, call
typewriter.SkipToEnd()iftypewriter.IsTyping, or advance to the next line if typing is already done
public class DialogueManager : MonoBehaviour
{
[SerializeField] private TypewriterEffect typewriter;
private Queue<string> _lines = new Queue<string>();
public void StartDialogue(string[] lines)
{
_lines.Clear();
foreach (var line in lines)
_lines.Enqueue(line);
ShowNextLine();
}
public void OnPlayerConfirm()
{
if (typewriter.IsTyping)
typewriter.SkipToEnd();
else
ShowNextLine();
}
private void ShowNextLine()
{
if (_lines.Count == 0)
{
// Dialogue ended - hide the UI, etc.
return;
}
typewriter.ShowText(_lines.Dequeue());
}
}Wrapping up
The maxVisibleCharacters approach is the right foundation for typewriter effects in Unity with TMP. It's clean, rich-text safe, and easy to extend.
Hope this helps! If you have questions or want to show what you built, let us know.
Cheers, Daniel
Daniel is a junior programmer at Febucci Team. Specialized in tech art with a Computer Engineering background. Also co-founded and managed the first student video game development team in Italy with Gabriele.
Comments