Why Thai Script Breaks at Small Sizes, and What Fixes It
TL;DR: A Thai syllable can be nearly twice as tall as the letter it is built on. In the font this app uses, a stacked vowel and tone mark reach 1.083em above the baseline against 0.559em for a plain consonant, which means Thai needs a line height of at least 1.555 or the top of the stack is cut off. The other failure is finer: the white gap separating a tone mark from the vowel beneath it can be 0.034em, which is half a pixel at 16px.

Thai is not one row of ink
Latin text lives in a band. There are ascenders and descenders, but almost everything sits between two lines and a renderer that gets the band right gets the text right. Thai does not work that way. A vowel can sit above the consonant, another mark can sit on top of that vowel, and a third can hang below, all belonging to one syllable and all needing vertical room the consonant itself does not use.
This is not an edge case. Of the 626 words in this app's own corpus, 306 carry a mark above or below a consonant. That is 49 per cent. Twenty-five of them stack two marks above a single consonant, and five carry marks above and below at once: อยู่, รู้, ต่อสู้, ปู่ and ตู้. Roughly every other word you meet is taller than the letters it is spelled with.
So when Thai renders badly at small sizes, the cause is almost never the letters. It is what is happening above and below them.
How much taller
Every figure below was measured from the font file this site loads, Noto Sans Thai, at a thousand units to the em. Positions are given in em so they scale with whatever size you set.
| Ink | Reaches |
|---|---|
| ก, a plain consonant | 0.559em above the baseline |
| ส, the tallest of the plain ones | 0.584em |
| ป ฝ ฟ, the letters with a raised stem | 0.745em |
| a tone mark on a bare consonant | 0.847em |
| a tone mark lifted over ี, as in ที่ | 1.083em |
| ู, the deepest vowel below | 0.265em below the baseline |
| ฎ ฐ, the deepest descenders | 0.204em below |
The tallest real stack is 1.94 times the height of a plain consonant. A word like อยู่, with a vowel underneath and a tone mark above, spans 1.112em from its lowest ink to its highest. A layout that reserves room for ก and hopes for the best is reserving about half of what the script actually asks for.
The lift is not accidental. The font carries mark-to-mark positioning rules that push a tone mark up out of the way when a vowel is already there: 0.236em over ี, ื and ึ, 0.185em over ั, 0.177em over ิ. The font is doing its job. The question is whether the box it has been given is tall enough to hold the result.

The line height that cuts the tops off
This font declares an ascent of 1.061em, a descent of 0.450em and no line gap, so it asks for 1.511em of vertical space per line. A browser takes your line height, subtracts that 1.511, and splits the difference evenly above and below. Set a line height smaller than 1.511 and the difference is negative: the box becomes shorter than the font asked for, and the shortfall comes off both ends.
Run that arithmetic against a stack that reaches 1.083em and you get a hard threshold.
| Line height | What happens to ที่ |
|---|---|
| 1.15 | overflows the line box by 0.2025em |
| 1.4 | overflows by 0.0775em |
| 1.5 | overflows by 0.0275em |
| 1.555 | fits, with nothing to spare |
| 1.6 | clears by 0.0225em |
1.5 is a common default for body text and it is not enough. It misses by about a quarter of a pixel at 16px, which sounds like nothing until you remember that the mark being clipped is only 0.215em tall to begin with. Losing a quarter pixel off a mark that is three and a half pixels tall is losing seven per cent of the only thing distinguishing two tones.
Overflow is what decides whether that becomes visible damage. Ink outside a line box is not automatically cut off; it is cut off when some ancestor clips it, which any container with a fixed height and hidden overflow will do. That is why the same paragraph can look correct in one place and decapitated in another with identical CSS on the text itself.
The bottom is a different story. At line height 1.15 the box extends 0.2695em below the baseline and ู reaches 0.265em, so the deepest vowel below the line squeaks in even at that setting. The clipping is asymmetric: Thai loses its tone marks long before it loses its vowels.
The half-pixel that decides whether a mark is readable
Clipping is the coarse failure. The fine one is that a stacked mark and the vowel below it are separated by a very small amount of white, and that gap does not scale into anything usable until the text is reasonably large.
| Stack | White gap between the two marks |
|---|---|
| ั with a tone mark, as in นั่ง | 0.034em |
| ึ with a tone mark | 0.065em |
| ี with a tone mark, as in ที่ | 0.078em |
| ิ with a tone mark | 0.096em |
Take the tightest of those, 0.034em, and put it on a screen:
- at 12px the gap is 0.41px
- at 16px it is 0.54px
- at 24px it is 0.82px
- at 34px it is 1.16px
Below about 34px that separation is thinner than one device-independent pixel. It does not vanish, because rasterizers work in subpixels and on a high-density display there are two or three device pixels behind each one, but it stops being a clean white row and becomes a grey smudge whose survival depends on hinting, rounding and the exact fractional position of the baseline. The mark is still there in the outline. What is gone is the evidence that there are two marks rather than one thick one.
That is the mechanism behind the common report that Thai tone marks look like they were dropped. Usually nothing was dropped. Two marks merged.

What this app settled on
The two failures need two different answers, and Phayan uses both.
Anything that is a run of Thai prose gets a line height of 1.6, applied by a single rule rather than left to whatever a paragraph inherits. 1.6 is above the 1.555 threshold, so the stack fits inside its own line box and no amount of clipping downstream can reach it.
The teaching stages, where a single word is shown large, take the opposite route. They keep a tight line height and set overflow to visible, so the stack is allowed to spill out of its box instead of being trimmed to fit it. The glyph stage runs at 104px in a box 1.5em tall, nudged up by 0.106em, with the overflow left alone.
Size is handled separately. The word display has a floor of 24px, chosen because 24px is 18pt, which is where the accessibility guidelines stop treating text as small and drop the contrast requirement from 4.5:1 to 3:1. The measurement above gives the same floor a second justification: 24px is roughly where the gap inside a stacked mark reaches four fifths of a pixel and starts to survive rounding.
Phayan is a reading course, not a typography demo, and this is why the two things end up connected: it teaches the letters one at a time from zero, puts audio on every word, and keeps your progress on the device without an account. A course built on telling one tone mark from another cannot afford to render them at a size where they merge.
If you are setting Thai yourself
Four things, in order of how much they matter:
- Line height 1.6 or more on anything containing Thai. Not 1.5. The threshold in this font is 1.555 and other Thai faces sit near it.
- Do not clip. A container with a fixed height and hidden overflow will cut the tone marks off a line that is otherwise set correctly.
- Do not go below 16px for Thai you expect a learner to decode, and prefer 24px or more when the point of the text is the marks themselves.
- Set the font explicitly. A generic sans-serif hands the script to whatever the system picks, and the numbers above are properties of one typeface, not of Thai.
The same arithmetic applies wherever the script is rendered, whether that is Phayan, an Anki deck whose card template you style yourself, or a phone app like Ling. What changes between them is how much of it you are allowed to fix. If you can reach the CSS, the fix is one declaration. If you cannot, the only lever left is the size control, which is why the choice of Thai typeface matters more in apps than it does on the web.
Frequently asked questions
What line height does Thai text need?
At least 1.555 in the font Phayan uses, which in practice means setting 1.6. The font declares an ascent of 1.061em and a descent of 0.450em, so it asks for 1.511em per line, while a tone mark stacked on a vowel reaches 1.083em above the baseline. Work the half-leading through and 1.5 leaves the stack overflowing its line box by 0.0275em, which is enough to be trimmed by any ancestor that clips. Phayan applies 1.6 to every run of Thai prose through a single rule so no inherited value can undercut it.
Why do Thai tone marks look like they disappear at small sizes?
Usually they have not disappeared, they have merged with the vowel beneath them. In the font measured here, the white gap between ั and a tone mark above it is 0.034em, which works out at 0.41px at 12px type and 0.54px at 16px. Below roughly 34px that separation is thinner than a device-independent pixel, so it survives as a grey smudge rather than a clean break and the pair reads as one thick mark. Phayan shows teaching words at 24px or larger for exactly this reason.
What font size should Thai be shown at for a learner?
Treat 16px as the floor for reading and 24px as the floor for anything where the marks are the lesson. 24px is 18pt, the size at which accessibility guidance stops calling text small and relaxes the contrast requirement from 4.5:1 to 3:1, and it is also roughly where the gap inside a stacked mark reaches four fifths of a pixel and starts surviving rounding. Phayan floors its word display at 24px on both grounds. Ling and other phone apps make that decision for you, which is worth knowing when a mark looks wrong.
Does the typeface matter, or only the size?
Both, and the typeface decides what the size has to be. Every number in this article is a property of one font file rather than of Thai: a different face puts its tone marks at different heights, leaves a different gap under them, and declares a different ascent, so it will have its own minimum line height. The method carries over even though the values do not. Phayan loads a named Thai face rather than falling back to a generic sans-serif, so the numbers it designs against are the numbers that actually render.
Why does Thai look fine in one app and broken in another?
Because clipping is decided by the container, not by the text. Ink that overflows a line box is only cut off when some ancestor has a fixed height and hidden overflow, so identical text with identical line height renders whole in one layout and decapitated in another. Phayan handles the two cases separately: prose gets a line height above the threshold so nothing overflows in the first place, and the large single-word stages keep a tight line height but leave overflow visible so the stack can spill instead of being trimmed. In an Anki deck you can set both yourself in the card template.