Why Emoji and RTL Text Combine Unpredictably
9 min read · updated August 11, 2026
An emoji the model placed after an Arabic greeting shows up at the other end of the line, or on the far side of an English word that came after it. Nothing moved the emoji. It has no direction of its own, so its position was decided by two things it does not control: the strong characters on either side of it, and the paragraph direction.
Emoji have a bidi class, and it is neutral
Every Unicode character has a Bidi_Class property, published in DerivedBidiClass.txt. The overwhelming majority of pictographic emoji are class ON — Other Neutral — which is the same class as a bracket, an asterisk or a mathematical operator. Neutral means the algorithm has no information about the character’s direction and must derive it from context, using rules N1 and N2 described in rendering mixed Arabic and English text.
That is the whole mechanism. An emoji between two right-to-left words takes the right-to-left direction by N1 and sits where you put it. An emoji between two left-to-right words takes left-to-right by N1 and likewise. An emoji between a right-to-left word and a left-to-right word has contradictory context, N1 does not apply, and N2 hands it the paragraph direction — which is the case where the same string renders differently in different containers.
The same string, two paragraph directions
Uppercase for right-to-left characters, following UAX #9. The stored string is identical in both cases; only the paragraph direction differs.
storage (logical): ARABIC [emoji] Hello
classes: AL...AL ON L L L L L
--- paragraph direction RTL (level 0 is odd, base level 1) ---
N1: the emoji is between AL (R) and L. Directions differ.
N2: emoji takes the embedding direction -> R, level 1
"Hello" is raised to level 2 by I2
display: Hello [emoji] CIBARA
^ emoji sits to the LEFT of the Arabic,
i.e. between the two words, as written
--- paragraph direction LTR (base level 0) ---
N1: same contradiction, does not apply
N2: emoji takes the embedding direction -> L, level 0
the Arabic run is at level 1
display: CIBARA [emoji] Hello
^ Arabic reversed, emoji still between themIn this example the emoji stays between the two words either way, and that is worth noticing: most of the time the outcome is fine and people never think about it. The visible failure needs a slightly different shape — an emoji with neutrals or a number on one side, or an emoji at a run boundary next to punctuation.
storage: ARABIC WORDS [emoji]. Hello
the emoji, the period and the space form one neutral run
between the Arabic (R) and "Hello" (L)
RTL paragraph: N2 gives the whole neutral run R
display: Hello .[emoji] SDROW CIBARA
the emoji AND the period land on the far side of
the English word, which is not what was intendedThat is the bug people report. The emoji did not move on its own; it was swept along with the adjacent punctuation as a single neutral run, because N1 and N2 resolve runs of neutrals together rather than character by character.
The same shape explains a second common report: an emoji at the very end of a message appearing at the start of the line. The end of the paragraph counts as a strong character of the paragraph direction for N1’s purposes, so a trailing emoji placed after an English sign-off inside an Arabic paragraph has L on one side and R on the other, resolves by N2 to the paragraph direction, and is drawn at the left end of the line — past the English text it was meant to follow. Nothing about the emoji is special: a full stop in the same position behaves identically, and does so in the wandering-period example in rendering mixed Arabic and English text. The emoji only feels like a bug because a period at the far end of a right-to-left line looks normal to a native reader and a floating smiley does not.
Sequences: ZWJ, modifiers and variation selectors
A single displayed emoji is often several code points. This matters for direction because the joining characters have their own classes and because a sequence that gets broken renders as its parts.
- U+200D ZERO WIDTH JOINER is class
BN, boundary neutral. Rule X9 removes BN characters from consideration entirely, so a ZWJ sequence behaves as a unit for direction purposes. - U+FE0F VARIATION SELECTOR-16, which requests the emoji presentation of a character that also has a text presentation, is a non-spacing mark. Rule W1 gives a non-spacing mark the class of the character before it, so it inherits and does not create a boundary.
- Skin tone modifiers, U+1F3FB to U+1F3FF, follow their base character. They are also the reason two visually identical emoji can compare unequal — a separate problem covered in emoji skin tone modifier comparison bugs.
The practical consequence is that you must not split a string inside an emoji sequence. A truncation at a fixed code-point count, a chunker splitting model output, or a database column that cuts at a byte limit will separate a base from its modifier or leave a dangling ZWJ, and the two halves then resolve independently and can land on opposite sides of a line. Truncate at grapheme cluster boundaries, which is what a segmenter gives you.
The emoji that are not neutral
“Emoji are neutral” is a good rule and not a universal one. A handful of emoji-presentation characters carry other classes — arrows and mathematical symbols that acquired emoji presentation have the classes they always had, and the regional indicator symbols used to build flag sequences are not ON. Because a flag is two regional indicators, a flag emoji can act as a strong anchor for the neutrals around it in a way that a smiley never does, and that is a genuinely confusing difference to hit in the wild.
The useful habit is not to memorise the exceptions but to look the character up. If you have an emoji-adjacent ordering problem you cannot explain, find the code point in DerivedBidiClass.txt before assuming the emoji is neutral. The emoji properties themselves are defined separately, in Unicode Technical Standard #51, which is where sequence composition is specified — it does not assign bidi classes, and conflating the two documents is a common source of wrong answers.
What to do about it
In descending order of how much of the problem each one removes:
- Set the paragraph direction explicitly. N2 is the rule doing the damage, and N2 consults the paragraph direction. A container that declares it correctly makes most emoji placement come out right for free.
- Do not let the model place emoji at run boundaries. If the response mixes scripts, an instruction to put emoji at the end of a sentence rather than adjacent to a language switch removes the ambiguous case entirely.
- Isolate the run you care about, not the emoji. Wrapping the emoji itself in a
<bdi>does very little, because a neutral inside an isolate is still a neutral. Isolating the Latin phrase that follows it is what removes the contradictory context. - Use a mark as a last resort. U+200F RIGHT-TO-LEFT MARK immediately after the emoji gives the neutral run a strong right-hand neighbour and pins it. It works, it is invisible in your data, and the objections in formatting RTL text in a chat interface apply: keep it at the presentation layer.
- Segment before truncating. Independent of direction, and it prevents the worst version of this bug.