
WebXR developers are constantly pushing boundaries of what’s possible in the immersive web, not only through crafting experiences that are captivating in terms of visuals and interactions, but also inclusive and accessible to a wider range of users. Presentation of text is a big part of immersive experiences over the web, however text directionality that is not Left-to-Right (LTR) is often not supported and in many cases problematic. This leads to wrong (and in many cases confusing) presentation of Right-to-Left (RTL) text and information to end users. In this blog post I provide a short historical context of LTR and RTL text, presentation of RTL text using HTML and finally discuss the main focus of the blog: RTL text in WebXR.
Two tiny disclaimers that I need to make before we jump in:
- The definition of “text” can vary slightly throughout this post. When referring to HTML/CSS pages or elements, text as the one you are reading now is designed to be displayed in web browsers using the standard HTML Markup Language. In the Immersive Web however, hold your breath for some serious oversimplification, text is essentially bitmap fonts/images that are graphically rendered and presented via browsers in a form that is “readable text” for end users. The focus of this article is on how to present RTL text in an accurate/readable manner in immersive environments, but we also touch on how you can make use of HTML text elements within your immersive scene using DOM Overlays.
- Due to my native language being Arabic — As-salamu alaykum السلام عليكم (peace be upon you) everyone! — you may find that I mention Arabic more than other RTL languages, this is simply because I know a little more about it. That being said, all solutions and workaround presented should be generally applicable.
Early computer systems were primarily developed to support LTR writing systems based on the Latin alphabet that is widely prevalent in Western languages. Accordingly the Latin alphabet became the standard for digital encoding leading to a natural bias towards accommodating LTR scripts.
This LTR-centric approach posed significant challenges for languages with RTL scripts, such as Arabic, Persian/Farsi and Hebrew. RTL text presented unique difficulties in digital environments, including issues with rendering, cursor placement and text mixing. These challenges stemmed from the fundamental differences in directionality, ligatures, joining control (and more) between LTR and RTL scripts, requiring specialised support to ensure accurate display and manipulation. Then along came the hero that is Unicode — this marked a significant moment in addressing problems linked to RTL scripts in digital systems by providing a comprehensive framework for encoding characters from diverse writing systems, including RTL scripts. Unicode facilitated greater interoperability and compatibility across digital platforms by providing standardised character encoding and directionality guidelines. You can find much more detailed information online around this topic — see the very last section on this page for some cool resources.
Despite these advancements, the full integration of RTL support in digital tools remains a work in progress. With the web now becoming more immersive using more complex rendering systems, there is still an ongoing need for continued development and collaboration to ensure inclusivity and accessibility for all writing styles.
The Internalization Working Group at W3C have done a brilliant job in addressing the issue of RTL text presentation for “flat webpages” (which we define as web pages that are not immersive and mostly 2D in nature, like the one you are reading this blog post on right now). You can now easily display RTL text in your HTML pages using the dir
attribute. For your HTML pages, you can make use of the dir
attribute to handle text directionality. The dir
attribute ensures your text is flowing in the intended direction saving you the headache of worrying about the presentation of text on your web page.
For example you can set the whole page to be RTL if the content is predominantly RTL in nature:
. . .
. . .
Or only set certain elements within your page to be RTL if you’re intending to mix and match text directions:
You can also set dir
to auto
, which can be useful in cases where you’re not sure what direction the text will be (for example users input text in forms in which ever language they prefer):
See the W3C’s Inline markup and bidirectional text in HTML document for more RTL text scenarios such as usage in tables, mixing both RTL and LTR text and others.
But what does this whole section have to do with WebXR? DOM Overlays is the short and sweet answer. The WebXR Device API supports DOM Overlays that allow placing HTML elements on top of an immersive scene. Thus even if you are faced with challenges in displaying RTL text within the WebXR environment, DOM Overlays can be a good option to fall back on using the dir
attribute as you would for a flat web page (the “flat web” term — there it is again, gaining traction already!). If you are interested to learn more about subtitles over the DOM, I encourage you to read over this report by the Immersive Captions Community Group that provides great guidelines and best practices for immersive subtitles.
Before jumping into how you can present RTL text accurately in your WebXR experiences, it’s worth mentioning that presenting RTL text correctly is a complex problem that is not exclusive to WebXR. In previous experiences I found this to be the case in other tools such as image editing software and native game engines. To better understand why this is a complex problem, I would advise having a look at the W3C’s Arabic & Persian Layout Requirements document which is pretty epic in its level of detail. I say epic because even as a native Arabic speaker I was in awe of some of the details and requirements covered in this document.
So back to solutions for the Immersive Web… I usually use A-Frame and Three.js for WebXR development, and to my knowledge neither provide built in support. I played around with both tools to attempt to present Arabic text and I am happy to share my failed attempts with you:
Both methods result in what I like to call “broken flipped Arabic” where a word gets flipped (defaulting to the left to right direction) and broken into separate letters with no lines connecting them correctly. You can make out what the separate letters are but because the whole word/phrase is flipped it becomes challenging to read and make sense of it as connecting ligatures are crucial to understand the meaning of Arabic text.
Because rendering 3D RTL text accurately is not supported out of the box in these engines, community solutions and efforts are usually king. troika-text is usually my go to component — it’s available for both A-Frame and Three.js:
troika-text offers a wide range of customisation and manipulation options to the appearance of RTL text.
To use troika-text in A-Frame
All you have to do is add the following in the of your page:
Then simply use the
component within
. You can use this sample scene to recreate or modify the example below. Note I am using the a-environment component here as an extra to add nice scenery:
RTL Text in A-Frame
You can also mix different text directions using troika-text as I’ve done below with English and Arabic:
To use troika-text in Three.js
First ensure that Three.js is installed in your development environment. You can refer to the Three.js installation docs to get started. I used Vite for this demo — it was simple to setup and had what I needed with a built-in local server:
npm install three --save-dev
After installation we will need an empty Three.js scene, you can follow the documentation from Three.js or troika-text to get started with creating a scene. Alternatively you can use my empty, cube-less, scene below:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xFFB799);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 20, 100);
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
Now that our scene is 3D text-ready, we need to install troika-text:
npm install troika-three-text --save-dev
Then import the Text
class from troika-text at the top of your script:
import { Text } from 'troika-three-text'
And finally add our troika-text as follows:
const myText = new Text();
scene.add(myText);
myText.text = 'your RTL text here';
myText.fontSize = 20;
myText.sync();
Your full Three.js scene should look like this to recreate the image below:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { Text } from 'troika-three-text'const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xFFB799);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 20, 100);
controls.update();
const myText = new Text();
scene.add(myText);
myText.text = 'سلام';
myText.fontSize = 20;
myText.position.z = -5;
myText.position.y = 20;
myText.position.x = -20;
myText.color = 0x2A00FF;
myText.sync();
const myText2 = new Text();
scene.add(myText2);
myText2.text = 'שָׁלוֹם';
myText2.fontSize = 20;
myText2.position.z = -5;
myText2.position.y = 10;
myText2.position.x = 30;
myText2.color = 0x2A00FF;
myText2.sync();
const myText3 = new Text();
scene.add(myText3);
myText3.text = 'امن';
myText3.fontSize = 20;
myText3.position.z = -5;
myText3.position.y = 20;
myText3.position.x = -70;
myText3.color = 0x2A00FF;
myText3.sync();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
This is a very basic example, but you can edit more properties of course. See the troika-text demos page for more customisation options.
A hidden gem for displaying Arabic and Hebrew text in Three.js is the mapbox-gl-rtl-text plugin. A little trickier to install, but produces great results for Arabic text once up and running! I hope the following steps save you the marathon of trial and error that I had to endure to make use of it.
Firstly install the package:
npm install @mapbox/mapbox-gl-rtl-text --save-dev
Then import the following modules at the top of your Three.js script. You can re-use the empty, cube-less, Three.js scene above as a starting point:
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
import rtlText from '@mapbox/mapbox-gl-rtl-text';
Then add the path to your chosen font (I grabbed “Noto Kufi Arabic” from Google Fonts). Note that you would first need to convert your font file to JSON (the supported font format in Three.js). I found Facetype.js very useful to convert .tff fonts to JSON files (bear in mind the exact name of the file will depend on the font variant you select):
const myFontPath = 'path/to/Noto Kufi Arabic SemiBold_Regular.json'
We also need to define a couple of variables to expose the functions needed for the processing and final rendering of Arabic text:
const {applyArabicShaping, processBidirectionalText} = await rtlText;
const loader = new FontLoader();
Then define your RTL text and a couple other variables that store the processed RTL text:
const myRtlText = 'سلام';
const myRtlTextArabicShaping = applyArabicShaping(myRtlText);
const myRtlTextForRendering = processBidirectionalText(myRtlTextArabicShaping, []);
We should now have all the needed information to add to the font loader.load()
function as normal. Note that the processBidirectionalText()
function takes an input string (i.e. the myRtlText
variable) along a set of chosen line break points for the displayed RTL text, and returns an array with an ordered set of lines with characters in the order they are displayed. Line breaks can be specified manually or by using \n
within the RTL text variable. This is particularly useful if you are trying to display long form text or paragraphs with known break points. Since we are not using any line breaks for our text in this case, we will need to pass the first element of this array to the TextGeometry()
function:
loader.load(myFontPath, function (font) {
const textGeometry = new TextGeometry(myRtlTextForRendering[0], {
font: font,
size: 10,
height: 0.1,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
});const textMaterial = new THREE.MeshNormalMaterial({ color: 0xffffff });
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
textMesh.position.x = -20;
scene.add(textMesh);
});
Your final script should look something like this and render the Arabic word for “Peace” in the centre of the scene:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
import rtlText from '@mapbox/mapbox-gl-rtl-text';const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xFFB799);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 20, 100);
controls.update();
const myFontPath = 'path/to/Noto Kufi Arabic SemiBold_Regular.json'
const {applyArabicShaping, processBidirectionalText} = await rtlText;
const loader = new FontLoader();
const myRtlText = 'سلام';
const myRtlTextArabicShaping = applyArabicShaping(myRtlText);
const myRtlTextForRendering = processBidirectionalText(myRtlTextArabicShaping, []);
loader.load(myFontPath, function (font) {
const textGeometry = new TextGeometry(myRtlTextForRendering[0], {
font: font,
size: 10,
height: 0.1,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
});
const textMaterial = new THREE.MeshNormalMaterial({ color: 0xffffff });
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
textMesh.position.x = -20;
scene.add(textMesh);
});
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
This library produces great results as it can accommodate for different fonts as demonstrated below. When it comes to Arabic in particular, customisation is usually much needed due to the breadth of calligraphy styles the language offers.
In Three.js, make sure you are using a font that supports RTL text in your font loader.load()
function. You may be faced with the infamous question marks that replace your characters if the font used does not support RTL text.
troika-text is not a perfect solution (yet). It may fail at times to connect letters when using some custom fonts (the text wasn’t flipped, but it separated the letters as seen below). That being said, it always works as expected with the default font.
While the mapbox-gl-rtl plugin is a great option for using custom fonts, it can also at times fail with some cursive Arabic fonts.
As you can probably tell by now, support for RTL text in the immersive web is one that requires continued development and it is worth keeping an eye on future developments in this space. Because this topic is closely linked to Internationalization, it’s important to keep the following in mind for your WebXR applications:
- Internationalization and Localisation are not the same…
- Internationalization refers to designing/developing your application or experience in a way that ensures it works for, and can be easily adaptable by, users from different cultures, regions and languages. It should be considered while designing and building your experience/environment to avoid unforeseen barriers.
- Localisation on the other hand is the stage that succeeds internationalization where you adapt your experience for different users (e.g. offer different languages, change colour schemes, convert currencies etc.).
- Internationalization of your immersive experiences goes beyond translation or text directionality, it also includes components such as cultural differences, time zones and currencies. See the W3C’s webpage on Internationalization for more on this topic.
I hope this post provides a starting point for you to develop more inclusive and accessible experiences for the masses worldwide!