Build websites 10x faster with HextaUI Blocks — Learn more
UI/Getting Started

RTL Setup

How to implement Right-to-Left (RTL) language support for your components.

Many applications need to support both Left-to-Right (LTR) and Right-to-Left (RTL) layouts to accommodate global audiences, particularly for languages like Arabic, or Persian.

HextaUI uses @radix-ui/react-direction to make this process seamless. This guide will walk you through setting up the direction provider, applying RTL-friendly styles, and handling direction-specific logic in your components.

1. Provider Setup

First, install the Radix Direction package. This provides the core context provider and hook for managing layout direction.

pnpm add @radix-ui/react-direction
# or
npm install @radix-ui/react-direction
# or
yarn add @radix-ui/react-direction

Next, wrap your application or layout with the DirectionProvider. This component provides the direction ("ltr" or "rtl") to all its children via React context.

"use client";

import { useState } from "react";
import { DirectionProvider } from "@radix-ui/react-direction";

export const Layout = ({ children }) => {
  const [direction, setDirection] = useState<"ltr" | "rtl">("ltr");

  const toggleDirection = () => {
    setDirection((prev) => (prev === "ltr" ? "rtl" : "ltr"));
  };

  return (
    // 1. Radix's provider makes the direction available via React context.
    <DirectionProvider dir={direction}>
      {/* 2. The `dir` attribute on a DOM element tells the browser and CSS how to render content. */}
      <div dir={direction}>
        <header className="p-4 border-b">
          <button
            onClick={toggleDirection}
            className="px-3 py-1 border rounded"
          >
            Toggle to {direction === "ltr" ? "RTL" : "LTR"}
          </button>
        </header>
        <main className="p-4">
          {children}
        </main>
      </div>
    </DirectionProvider>
  );
};

2. Implementing RTL Styles

With the dir attribute set, you can now use modern CSS techniques to create layouts that automatically adapt to the direction.

Method A: Logical Properties (The Easy Way)

The best practice is to use logical properties in TailwindCSS. Instead of specifying left or right, you use start and end. The browser then correctly interprets these based on the dir attribute.

Directional Class (ltr-only)Logical Alternative (Adapts to ltr/rtl)
ml- / mr-ms- (margin-start) / me- (margin-end)
pl- / pr-ps- (padding-start) / pe- (padding-end)
border-l / border-rborder-s / border-e
rounded-l / rounded-rrounded-s / rounded-e
text-left / text-righttext-start / text-end
left- / right-start- / end-

Method B: Directional Variants (rtl: and ltr:)

For CSS properties that don't have a logical equivalent (like transform or rotate), Tailwind provides rtl: and ltr: variants. Use these to apply styles only for a specific direction.

This is perfect for flipping icons or adjusting complex animations.

// Example: An icon that should always point "forward".
import { ChevronRightIcon } from '@heroicons/react/20/solid';

<ChevronRightIcon className="size-4 rtl:-scale-x-100" />
  • In LTR mode, the icon points right: ➡️
  • In RTL mode, the rtl:-scale-x-100 class activates, flipping it to point left: ⬅️

Here is another practical example from a Switch component, where a "thumb" element needs to slide in opposite directions based on the layout.

// A thumb element inside a Switch component
<div
  className="h-4 w-4 rounded-full bg-white shadow transition-transform
             data-[state=checked]:translate-x-4
             rtl:data-[state=checked]:-translate-x-4
             data-[state=unchecked]:translate-x-0"
/>

Let's break down how the directional logic works here:

  • data-[state=checked]:translate-x-4: This is the default LTR behavior. When the component's data-state is "checked", the thumb moves 4 units to the right.
  • rtl:data-[state=checked]:-translate-x-4: This is the RTL override. When the dir is "rtl", this class activates and applies a negative translate-x, moving the thumb 4 units to the left instead.
  • data-[state=unchecked]:translate-x-0: When the state is "unchecked", the thumb returns to its starting position (no translation) in both LTR and RTL.

3. Handling Direction in JavaScript

Sometimes CSS isn't enough. You may need to change component logic, render different elements, or adjust animation parameters based on the direction. For these cases, use the useDirection hook from @radix-ui/react-direction.

This approach is necessary because many JavaScript animation libraries, including Framer Motion, do not support CSS logical properties like marginInlineStart directly in their animation APIs. They require explicit physical properties (marginLeft, marginRight).

The useDirection hook bridges this gap. It allows you to programmatically provide the correct physical property to the animation function, ensuring your motion logic is perfectly synchronized with the layout direction.

Consider a main content area that needs to animate its margin to make space for a collapsible sidebar. The margin that needs to be animated (marginLeft vs. marginRight) depends entirely on the layout direction.

import { useDirection } from '@radix-ui/react-direction';
import { motion } from 'framer-motion';

export const MainContent = ({ isSidebarCollapsed, children }) => {
  const direction = useDirection();
  const sidebarWidth = isSidebarCollapsed ? '4rem' : '16rem'; // e.g., 64px or 256px

  // 1. Determine the animation properties based on the current direction.
  const animateProps = direction === 'rtl'
    // In RTL, the sidebar is on the right, so we animate `marginRight`.
    ? { marginRight: sidebarWidth, marginLeft: 0 }
    // In LTR, the sidebar is on the left, so we animate `marginLeft`.
    : { marginLeft: sidebarWidth, marginRight: 0 };

  return (
    // 2. Pass the dynamic props to the motion component.
    <motion.main
      className="flex-1"
      initial={false}
      animate={animateProps}
      transition={{ duration: 0.3, ease: 'easeInOut' }}
    >
      {children}
    </motion.main>
  );
};

In this example:

  • We get the current direction ("ltr" or "rtl") using the useDirection hook.
  • We use a ternary operator to dynamically create the animateProps object.
  • If the direction is rtl, we apply the animated margin to the marginRight property.
  • If the direction is ltr, we apply it to marginLeft.
Edit on GitHub

Last updated on