Most modern digital products are originally designed for Left-to-Right (LTR) languages such as English, French, and Spanish. As a result, many popular streaming and media applications are built assuming LTR layouts, navigation patterns, and interaction models.
When these applications expand into regions where Right-to-Left (RTL) languages such as Arabic and Hebrew are dominant, supporting RTL becomes a critical requirement not just as a visual change, but as a core usability feature.
RTL (Right-to-Left) support often looks simple until you ship it.
If you’ve built web apps, you might think RTL is just about flipping alignment and setting dir="rtl" or using CSS logical properties. On modern browsers, rendering engines handle most of the hard parts such as text shaping, punctuation placement, BiDi ordering, cursor movement, and truncation.
TV apps are a different world.
In TV applications, RTL affects not only layout but also the entire interaction model:
- Focus navigation
- Remote control semantics (Left/Right behaviour changes)
- Pointer remotes like LG Magic Remote (mouse coordinates need transformation)
- Text shaping and BiDi consistency (mixed Arabic + English + numbers)
- Old TV browser quirks that don’t behave like modern Chromium
This blog covers the production approach we implemented in Lightning.js. It’s not a theoretical “RTL best practices” guide it’s the exact sequence of problems we hit after enabling RTL and how we solved them in a way that scaled.
This work was carried out as part of our engineering efforts at Diagnal.
Lightning.js
Lightning.js is an open-source JavaScript framework designed specifically for building high-performance Smart TV applications. It uses WebGL for rendering instead of traditional HTML and CSS, enabling smooth animations, efficient memory usage, and consistent performance across constrained TV hardware.
We’ve used Lightning.js 2.0 along with the Lightning SDK to build and maintain large-scale production TV applications across multiple platforms, including Samsung Tizen, LG webOS, and Vidaa OS.
Understanding RTL and LTR Layouts

What is RTL and LTR (and why it changes the UI)
Before going into implementation, it helps to clarify what RTL really means in UI terms.
LTR (Left-to-Right)
Languages like English, French, Spanish are LTR. Content flows:
- text: left → right
- reading order: start on the left
- UI patterns: “next” usually points right
RTL (Right-to-Left)
Languages like Arabic and Hebrew are RTL. Content flows:
- text: right → left
- reading order: start on the right
- UI patterns often mirror: “next” points left, back points right
Where things get tricky: mixed content (BiDi)
Real production content is almost always mixed:
- Arabic titles containing English names
- timestamps and numbers
- season/episode codes
- punctuation (quotes, commas, brackets)
Example:
الحلقة S02E05 - Episode 5This is called BiDi (bidirectional text), and getting it right is usually where most RTL bugs appear.
RTL on Web Apps (React) vs TV Apps (Lightning.js)
This is an important difference, because people often assume TV RTL is the same as web RTL.
RTL in React / web apps
In React apps, RTL is typically solved using:
dir="rtl"on<html>or container- CSS logical properties (
margin-inline-start,padding-inline-end) - mature browser engines with stable BiDi
- libraries like MUI / Chakra / RTL plugins
So the browser does most of the heavy lifting.
RTL in Lightning.js / TV apps
Lightning.js apps are not DOM layout apps. Your UI is:
- canvas-based rendering
- focus-driven navigation
- custom layout logic
- separate pipeline for textures and text rendering
On top of that, the “browser” running Lightning may be:
- old Chromium
- old WebKit
- vendor fork with missing Intl / BiDi features
So RTL becomes a combination of:
- rendering transformation
- input transformation
- platform hardening
Challenges While Implementing RTL
Why Lightning’s RTL prop wasn’t enough for us
Lightning.js provides an rtl prop you can pass. It can invert a component tree.
But in production we found two practical issues:
No end-to-end RTL behaviour
Component inversion is only one piece.
You still have to handle:
- text direction
- icons
- focus
- input
- pointer remotes
- etc.
Scaling is hard
For complex component trees, relying on rtl at multiple component levels becomes messy.
- Teams start adding RTL patches per component.
- Consistency becomes difficult to enforce.
We needed something that worked at the architecture level, not something every screen developer has to implement manually.
Implementation
- How We Utilized Canvas Transform Property
- Remote Key and Focus Handling
- Mouse Handling and Coordinate Transformation
- Using the Drafted PR from Lightning JS GitHub
- Creating Our Own Repository
Implementation and Strategy
Flip the Canvas
After trying a few approaches, the best and most scalable strategy we found was:
Flip the canvas itself.
This mirrors the entire application UI instantly, including layout, rails, and animations—without rewriting every component.
Canvas flip
const canvas = this.stage.getCanvas();
canvas.style.transform = checkRTL() ? 'scaleX(-1)' : 'scaleX(1)';Once this was done:
- RTL layout was achieved globally
- we avoided most manual x-position inversion
- focus layout automatically mirrored visually

Images getting mirrored
After flipping the canvas, every image is mirrored too:
- Posters look flipped
- Chevrons point the wrong way
- Logos become unreadable
What we did
We created a common ImageComponent and flipped images again using scaleX.
// ImageComponent
this.Image.scaleX = checkRTL() ? -1 : 1;This kept the layout mirrored, but preserved image orientation.
One more important point: RTL assets
Even after flipping back images, there is a bigger problem:
Most images are designed for LTR. That means they may still look wrong in RTL because design itself is directional.
For example:
- arrows
- “forward” icons
- progress indicators
- artwork with direction implied
The right solution: serve RTL-specific assets from MW/backend.
So MW can respond differently based on payload locale like ar-AE, and return:
- RTL icons
- RTL banners/artwork
- RTL chevrons
That keeps the client lightweight and avoids messy runtime transforms.

Remote navigation felt inverted
Canvas flip mirrors visuals, but remote key codes don’t change.
So:
- Right key moves focus in the opposite direction visually
- Left key does the opposite
This is a major UX mismatch.
What we did
We intercepted keyboard events and swapped Left/Right arrow key codes when RTL is enabled.
const RTLKeySwap = (event: KeyboardEvent) => {
if (!checkRTL() || (event as any).isSwapped) return true;
if (![37, 39].includes(event.keyCode)) return true;
const swappedKeyCode = event.keyCode === 37 ? 39 : 37;
const swappedKey = swappedKeyCode === 39 ? 'ArrowRight' : 'ArrowLeft';
const newEvent = new KeyboardEvent(event.type, {
key: swappedKey,
code: swappedKey,
keyCode: swappedKeyCode,
bubbles: true,
cancelable: true,
}) as any;
Object.defineProperty(newEvent, 'isSwapped', { value: true });
event.preventDefault();
event.stopPropagation();
document.dispatchEvent(newEvent);
return false;
};
This fix immediately reduced focus workarounds and made navigation feel natural.
Text got mirrored
Texts were mirrored because canvas was flipped.
This is not a BiDi issue—it’s simply rendering orientation.
What we did
We introduced a common TextBox base component and flipped it again:
// in shared TextBoxthis._scaleX=checkRTL() ?-1:1;
Punctuation, comma, time formats had issues (Fix: BiDi handling + RTL markers)
Even after fixing mirroring issue, text still wasn’t fully correct across devices.
We observed issues with:
- Commas
- Single quotes / double quotes
- Time format strings
- Mixed Arabic + English titles
- BiDi ordering
What we did
We added a convertToRTLText() helper that injects Unicode direction markers to stabilize rendering.
const convertToRTLText = (text: string) => {
const RLM = '\u200F';
const LRM = '\u200E';
return `${RLM}${text}${LRM}`;Applied as:
<code>const displayText = checkRTL() ? convertToRTLText(text) : text;</code>This wasn’t only theoretical — it fixed a lot of real production punctuation formatting issues that were device-specific.

LG Magic Remote pointer was offset
LG Magic Remote behaves like mouse pointer input.
Once the canvas is mirrored, pointer coordinates do not line up with UI.
Hover and click feel shifted.
What we did
We transformed hover/click coordinates by mirroring X using canvas rect.
<code>application.receiveHover = (event: any) => { const rect = canvas.getBoundingClientRect(); const precision = (window as any).resolutionConfig?.precision || 1; const x = event.clientX / precision; const mirroredX = rect.width - x - rect.left; const transformedEvent = { ...event, clientX: (rect.left + mirroredX) * precision, movementX: -event.movementX, }; fireBottomUpHoverHandlerDirect(transformedEvent); };</code>Same logic applies to click events.
This fix is required for pointer remote usability.
Old TVs: scaleX, direction, RTL/LTR weren’t reliable
After solving almost everything, we discovered the worst issue on older TV browsers: text rendering itself was broken.
On some older TVs:
scaleX(-1)was inconsistentdirection: rtl/ltrbehaved unpredictably- RTL/LTR shaping and rendering were unreliable
- in several cases, Arabic text didn’t render at all and appeared as boxes (□ □ □)
This isn’t something you can fix with small patches – because if the base transform and text rendering layer are unstable, the entire RTL approach becomes unreliable.
What we did
There was no “clean” solution.
At this point the only options are:
- Implement RTL manually everywhere, OR
- Patch/fork core behavior to ensure consistent results
Lightning did have a PR, but it still contained issues around:
- BiDi text
- Punctuation
- Time formatting
- Quotes handling
So we took the practical route: fork Lightning core and handled the missing pieces ourselves, based on our product requirements.
That includes the minor-but-critical fixes that make RTL “feel correct”.
Why we created a custom Lightning branch
To ship RTL reliably across our device matrix, we maintained a custom branch:
Before using this branch, please make sure to test it thoroughly. We have also added a sample application for validation and testing purposes.
This branch has been primarily tested with Arabic and English mixed content. However, it has not been extensively tested with other RTL layout–based languages.
This allowed:
- Controlled iteration
- Stable releases
- Targeted RTL fixes without waiting for upstream changes
- Compatibility hardening for specific TV models
Upstream PR reference:
Conclusion
While implementing RTL support, we encountered several text rendering challenges. When truncating text in RTL, the ellipsis appeared on the left side instead of the right. Additionally, punctuation marks such as commas, parentheses, and slashes (/) were not rendered correctly by default. These issues were addressed directly in our forked Lightning branch.
Beyond text rendering, we also resolved several other critical RTL-related challenges, including:
- Global layout mirroring
- Image and text mirroring
- Remote navigation mismatches
- Pointer coordinate mismatches
- Punctuation and BiDi string issues
- Incorrect truncation direction
- Limitations on older TV models
The canvas flip strategy gave us the fastest and cleanest foundation. From there, shared components (ImageComponent, TextBox) and app-level input hooks made RTL consistent and scalable.
When older TV platforms broke the transform-based approach, forking the core became the only reliable production solution.
We hope these learnings help other Lightning.js teams avoid common RTL pitfalls and ship more robust, global-ready TV applications. Feel free to reach out to the Diagnal team to learn more or share what you’re working on!







