47.6 RTL Support and Direction-Aware Types
Right, so you’ve built a beautiful, responsive UI. It looks perfect. Until someone views it in a language that reads right-to-left, like Arabic or Hebrew, and suddenly your meticulously placed “Add to Cart” button is floating in the void like a lost astronaut. The web is a global place, and assuming left-to-right (LTR) is a fantastic way to look amateurish to a huge portion of your audience.
Thankfully, CSS does most of the heavy lifting here with the dir attribute and the direction property. Our job in TypeScript isn’t to reimplement that logic, but to model it, to make our components direction-aware and our functions robust enough to handle the flip. We’re adding type-safety to a design paradigm.
The Foundation: CSS Logical Properties
Before we write a line of TypeScript, let’s get our CSS straight. The old way was to use physical properties like margin-left and padding-right. The modern, RTL-safe way is to use CSS Logical Properties. These are based on the flow of the document, not a physical direction.
.old-way {
margin-left: 1rem; /* Bad. Always left, even in RTL. */
text-align: right; /* Also bad. */
}
.new-way {
margin-inline-start: 1rem; /* Magically becomes margin-right in RTL */
text-align: end; /* Becomes 'right' in LTR, 'left' in RTL. Perfect. */
}
Your first and most important step is to use margin-inline-start/end, padding-inline-start/end, inset-inline-start/end, and text-align: start/end everywhere you possibly can. It will save you an astronomical amount of debugging time. I’m not kidding. This is the single biggest win.
Modeling Direction in Your Types
Now, for TypeScript. We need a single source of truth for the current direction. A simple union type is our best friend.
type WritingDirection = 'ltr' | 'rtl';
This seems trivial, but its power is in enforcement. Now you can create functions and components that explicitly require or use this type.
// A function to get the opposite direction, useful for toggles
function getOppositeDirection(dir: WritingDirection): WritingDirection {
return dir === 'ltr' ? 'rtl' : 'ltr';
}
// Use it in a React component prop
interface DirectionAwareComponentProps {
direction: WritingDirection;
// ... other props
}
This prevents you from accidentally passing 'left' or some other nonsense string. You’ve now made invalid states unrepresentable.
The Conditional Flips: Handling RTL-Specific Logic
Sometimes, logic itself needs to change. A classic example is a carousel. In LTR, clicking “next” should show the slide to the right. In RTL, “next” should show the slide to the left. The semantic meaning of “next” stays the same, but the visual implementation changes.
Here’s how you might model a function that calculates the transform value for a slide:
function getSlideTransform(index: number, direction: WritingDirection): string {
// The core logic flips the multiplier based on direction
const multiplier = direction === 'ltr' ? 1 : -1;
return `translateX(${index * 100 * multiplier}%)`;
}
// Usage:
// getSlideTransform(1, 'ltr') // => 'translateX(100%)' (moves right)
// getSlideTransform(1, 'rtl') // => 'translateX(-100%)' (moves left)
The beauty here is that the function is pure and deterministic. Given the same index and direction, it will always return the correct value. It’s a single, tested piece of logic instead of if/else statements scattered throughout your component code.
The Pitfall: Icons and Asymmetric Assets
This is where most people get bitten. You have an icon of a chevron pointing right to mean “forward”. You slap transform: rotate(180deg) on it in an RTL context. Done, right? Wrong.
What about an icon of a book with a bookmark on the right side? Or a profile picture with a “notification” dot on the right? Or a logo that isn’t symmetrical? You can’t just rotate these. For these asymmetric assets, you often need a separate, flipped image.
Your type system can help you remember to handle these cases.
type IconType = 'chevron' | 'asymmetric-bookmark' | 'profile-notification';
function getIconSrc(type: IconType, direction: WritingDirection): string {
const basePath = '/assets/icons/';
if (type === 'chevron') {
// Chevron is symmetric, we just use the same one for both
return `${basePath}chevron-right.svg`;
}
if (type === 'asymmetric-bookmark') {
// For the asymmetric icon, we need a different file
const suffix = direction === 'ltr' ? 'ltr' : 'rtl';
return `${basePath}bookmark-${suffix}.svg`;
}
// ... handle other types
}
By forcing yourself to write this function, you’re forced to think about each icon individually. The type IconType acts as a checklist. You can’t just forget about the bookmark icon; the compiler will ask you to handle it in the function.
Reading the Direction from the DOM
Finally, you need a reliable way to get the current direction from your app’s state or the DOM itself. Here’s a simple utility function to get it from the html or body element:
function getDocumentDirection(): WritingDirection {
// Check for the dir attribute on the <html> or <body> element
const dirAttribute = document.documentElement.getAttribute('dir') ||
document.body.getAttribute('dir');
// Default to 'ltr' if not explicitly set, per spec.
return dirAttribute === 'rtl' ? 'rtl' : 'ltr';
}
You can integrate this with a React context or a state management library to make this value globally available to your entire application, triggering re-renders when it changes (e.g., via a language selector dropdown).
The goal isn’t to make your code a maze of direction checks. It’s to centralize the logic, model the possibilities in your type system, and use the tools CSS already gives you. Do that, and your UI will gracefully flip its way across the globe.