Índice
Soporte de derecha a izquierda (RTL): Guía práctica de implementación con CSS y React
El soporte RTL es una de esas funcionalidades que se pospone hasta que ya no es posible ignorarla. Entonces los equipos descubren que han construido un producto completo que asume que el texto fluye de izquierda a derecha, y adaptar RTL implica tocar cada archivo de maquetación que tienen.
Esta guía trata de hacerlo bien desde el principio — o al menos, de realizar la adaptación de la forma más limpia posible. Cubriremos las propiedades lógicas de CSS, el atributo dir, el comportamiento de flexbox, patrones de React, utilidades de Tailwind y todo lo que hay entre medias.
¿Qué idiomas necesitan RTL?
Antes de la implementación, comprende a tu audiencia. Los sistemas de escritura RTL incluyen:
- Árabe — más de 400 millones de hablantes nativos, oficial en 26 países
- Hebreo — más de 10 millones de hablantes, dominante en un mercado de alto PIB
- Persa/Farsi — más de 80 millones de hablantes en Irán, Afganistán y Tayikistán
- Urdu — más de 70 millones de hablantes nativos, cooficial en Pakistán
- Pastún — más de 60 millones de hablantes
- Sindhi, Uigur, Kurdo (Sorani) — bases de usuarios más pequeñas pero significativas
Los mercados árabe y hebreo por sí solos representan un enorme potencial de ingresos en e-commerce y SaaS. Los productos sin soporte RTL sencillamente no se despliegan en estas regiones — localizar el contenido sin localizar la maquetación carece de sentido.
Propiedades lógicas de CSS: La base
La mejora más importante que puedes hacer en tu CSS para el soporte RTL es cambiar de propiedades físicas a propiedades lógicas. Las propiedades físicas (margin-left, padding-right, border-left) están vinculadas a los bordes de la pantalla. Las propiedades lógicas (margin-inline-start, padding-inline-end, border-inline-start) se adaptan automáticamente según la dirección de escritura del documento.
Este único cambio resuelve aproximadamente el 80% de los problemas de maquetación RTL.
Tabla de correspondencia:
| Propiedad física | Equivalente lógico |
|---|---|
margin-left | margin-inline-start |
margin-right | margin-inline-end |
padding-left | padding-inline-start |
padding-right | padding-inline-end |
border-left | border-inline-start |
border-right | border-inline-end |
left | inset-inline-start |
right | inset-inline-end |
margin-top | margin-block-start |
margin-bottom | margin-block-end |
padding-top | padding-block-start |
padding-bottom | padding-block-end |
text-align: left | text-align: start |
text-align: right | text-align: end |
width | inline-size |
height | block-size |
Compatibilidad con navegadores en 2026: Excelente. Todos los navegadores principales llevan años soportando las propiedades lógicas de CSS. Puedes usarlas sin dudarlo.
/* Antes — falla en RTL */
.card {
margin-left: 1rem;
padding-right: 1.5rem;
border-left: 2px solid var(--accent);
text-align: left;
}
/* Después — funciona en LTR y RTL */
.card {
margin-inline-start: 1rem;
padding-inline-end: 1.5rem;
border-inline-start: 2px solid var(--accent);
text-align: start;
}
En modo LTR, margin-inline-start se resuelve como margin-left. En RTL, se resuelve como margin-right. Escribes el CSS una vez y ambas direcciones funcionan correctamente.
El atributo dir
Establece la dirección en el nivel del elemento HTML:
<html lang="ar" dir="rtl">
O por elemento cuando tienes contenido mixto:
<p dir="rtl">مرحباً بالعالم</p> <p dir="ltr">Hello world</p>
La propiedad CSS direction hace lo mismo pero en CSS:
.arabic-content {
direction: rtl;
}
El patrón de selector [dir="rtl"] es útil para sobrescrituras específicas de RTL cuando las propiedades lógicas no son suficientes:
/* Por defecto (LTR) */
.nav-icon {
transform: none;
}
/* Sobrescritura RTL para iconos direccionales */
[dir="rtl"] .nav-icon--arrow {
transform: scaleX(-1);
}
unicode-bidi: bidi-override fuerza una dirección específica independientemente del contenido. Úsalo con moderación — normalmente solo es necesario para bloques de código o contenido que no debe reordenarse:
.code-block {
direction: ltr;
unicode-bidi: isolate;
}
Flexbox y Grid en RTL
Hay una buena noticia genuina: flexbox y CSS grid respetan automáticamente la dirección del documento.
.nav {
display: flex;
flex-direction: row;
gap: 1rem;
}
En LTR, los elementos fluyen de izquierda a derecha. En RTL, el mismo CSS hace que los elementos fluyan de derecha a izquierda. No necesitas cambiar nada.
Lo que funciona automáticamente:
flex-direction: rowse invierte en RTLjustify-content: flex-startalinea al inicio en línea (a la derecha en RTL)- El orden de columnas del Grid respeta la dirección
- Las áreas de plantilla del Grid se adaptan correctamente
Lo que necesita atención manual:
/* Esta transformación no se invertirá automáticamente */
.slide-in {
transform: translateX(-100%);
}
/* Arregla con el equivalente de propiedad lógica */
[dir="rtl"] .slide-in {
transform: translateX(100%);
}
El posicionamiento con left/right tampoco se invertirá:
/* Falla en RTL */
.tooltip {
position: absolute;
left: 100%;
}
/* Usa propiedades lógicas en su lugar */
.tooltip {
position: absolute;
inset-inline-start: 100%;
}
Patrones comunes que necesitan correcciones RTL
Flechas de navegación y chevrons
Los iconos de flecha que indican dirección deben invertirse en RTL:
/* Invertir solo iconos direccionales */
[dir="rtl"] .icon--arrow-right,
[dir="rtl"] .icon--chevron-right,
[dir="rtl"] .icon--next,
[dir="rtl"] .icon--forward {
transform: scaleX(-1);
}
NO invertir: marcas de verificación, triángulos de advertencia, logotipos, avatares de usuario, iconos de subida/bajada, controles multimedia (reproducir/pausar). Estos no son direccionales.
Migas de pan (Breadcrumbs)
El separador entre los elementos de las migas de pan debe invertirse:
.breadcrumb-separator::before {
content: "/";
}
[dir="rtl"] .breadcrumb-separator::before {
content: "\\";
/* O usa un separador neutral en RTL como • */
}
Mejor opción: usa un carácter separador neutro o un SVG que puedas invertir.
Barras de progreso
La dirección de relleno importa visualmente:
.progress-fill {
width: var(--progress);
/* En LTR: se rellena desde la izquierda */
/* En RTL: debería rellenarse desde la derecha */
transform-origin: inline-start;
}
[dir="rtl"] .progress-fill {
margin-inline-start: auto;
/* O: usa un gradiente reflejado */
}
Sombras de caja
Las sombras que indican profundidad o elevación a menudo parecen direccionales:
/* LTR — sombra a la derecha */
.card {
box-shadow: 4px 0 8px rgba(0,0,0,0.1);
}
/* RTL — la sombra debería estar a la izquierda */
[dir="rtl"] .card {
box-shadow: -4px 0 8px rgba(0,0,0,0.1);
}
Implementación en React
Estructura el estado de dirección como un contexto para que cualquier componente pueda consumirlo:
// direction-context.tsx
import { createContext, useContext, ReactNode } from "react";
type Direction = "ltr" | "rtl";
const DirectionContext = createContext<Direction>("ltr");
interface DirectionProviderProps {
direction: Direction;
children: ReactNode;
}
export function DirectionProvider({ direction, children }: DirectionProviderProps) {
return (
<DirectionContext.Provider value={direction}>
<div dir={direction} className={`dir-${direction}`}>
{children}
</div>
</DirectionContext.Provider>
);
}
export function useDirection(): Direction {
return useContext(DirectionContext);
}
En la raíz de la aplicación, pasa la dirección desde tu configuración de locale:
// app.tsx
import { DirectionProvider } from "./direction-context";
const RTL_LOCALES = new Set(["ar", "he", "fa", "ur"]);
export function App({ locale }: { locale: string }) {
const direction = RTL_LOCALES.has(locale) ? "rtl" : "ltr";
return (
<DirectionProvider direction={direction}>
<Router />
</DirectionProvider>
);
}
Los componentes que necesitan conocer la dirección consumen el hook:
function NavArrow() {
const direction = useDirection();
return (
<svg
style={{
transform: direction === "rtl" ? "scaleX(-1)" : "none"
}}
aria-hidden="true"
>
{/* arrow path */}
</svg>
);
}
Con CSS Modules: añade un atributo de datos y selecciona con él:
<div data-direction={direction} className={styles.container}>
/* component.module.css */
.container {
padding-inline-start: 1rem;
}
/* Sobrescribir solo lo que las propiedades lógicas no pueden manejar */
.container[data-direction="rtl"] .icon {
transform: scaleX(-1);
}
Cuando usas una plataforma como Better i18n, la dirección del locale está disponible desde el SDK junto con el propio locale, por lo que puedes derivar dir en el mismo punto en que derivas el locale sin ninguna configuración adicional.
Soporte RTL en Tailwind CSS
Tailwind incluye las variantes rtl: y ltr: de serie. Actívalas en tu configuración:
// tailwind.config.js
module.exports = {
// ...
future: {
hoverOnlyWhenSupported: true,
},
};
Las variantes se activan cuando un elemento ancestro tiene dir="rtl" o dir="ltr".
Uso básico:
<!-- El margen cambia según la dirección --> <div class="ms-4 me-2"> <!-- ms- = margin-inline-start, me- = margin-inline-end --> <!-- Estas son las utilidades de propiedad lógica de Tailwind --> </div> <!-- Sobrescrituras direccionales explícitas --> <button class="ltr:ml-4 rtl:mr-4">Submit</button> <!-- Inversión de iconos --> <svg class="rtl:scale-x-[-1]">...</svg>
Las utilidades de propiedad lógica de Tailwind (ms-, me-, ps-, pe-, border-s, border-e, start-, end-) son la opción correcta por defecto — reemplazan las utilidades físicas y gestionan RTL sin variantes en absoluto:
<!-- Antes: utilidades físicas, falla en RTL --> <nav class="pl-6 border-l-2 text-left"> <!-- Después: utilidades lógicas, funciona en ambos --> <nav class="ps-6 border-s-2 text-start">
Usa las variantes rtl: y ltr: solo para cosas que las propiedades lógicas no pueden manejar — como transformaciones de iconos u orígenes de animación.
Pruebas de maquetaciones RTL
Chrome DevTools
La forma más rápida de previsualizar RTL: abre DevTools, ve a la pestaña Rendering y busca "Emulate CSS media feature forced-colors" — pero más útil es simplemente añadir dir="rtl" al elemento raíz mediante el panel Elements. No es necesario recargar la página.
Playwright
Escribe pruebas que reconozcan la dirección y validen la maquetación en ambos modos:
// rtl.spec.ts
import { test, expect } from "@playwright/test";
test.describe("RTL layout", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
document.documentElement.setAttribute("dir", "rtl");
});
});
test("navigation renders correctly in RTL", async ({ page }) => {
const nav = page.locator("nav");
await expect(nav).toBeVisible();
// Verify the menu is on the right side in RTL
const navBox = await nav.boundingBox();
const viewportWidth = page.viewportSize()?.width ?? 1280;
expect(navBox!.x).toBeGreaterThan(viewportWidth / 2);
});
test("form inputs align correctly", async ({ page }) => {
const label = page.locator('label[for="email"]');
const input = page.locator("#email");
const labelBox = await label.boundingBox();
const inputBox = await input.boundingBox();
// In RTL, label should be to the right of or above the input
// This depends on your layout, adjust assertion accordingly
expect(labelBox).toBeTruthy();
expect(inputBox).toBeTruthy();
});
});
Regresión visual
Ejecuta pruebas de regresión visual con una variante dir="rtl". Con la comparación de capturas de pantalla integrada de Playwright:
test("RTL homepage snapshot", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
document.documentElement.setAttribute("dir", "rtl");
});
await expect(page).toHaveScreenshot("homepage-rtl.png");
});
Lista de verificación manual de QA
- Los elementos de navegación fluyen de derecha a izquierda
- La barra lateral está a la derecha
- Las etiquetas de formulario se alinean correctamente
- Los iconos direccionales (flechas, chevrons) están invertidos
- Las barras de progreso se rellenan desde la derecha
- Los menús desplegables se abren en la dirección correcta
- Los modales/cajones se abren desde el lado correcto
- Los separadores de migas de pan apuntan en la dirección correcta
- La alineación del texto es correcta en todo momento
- Las barras de desplazamiento aparecen a la izquierda (los navegadores lo gestionan automáticamente)
- Los números y el texto en latín permanecen LTR dentro del contenido RTL
Fuentes y tipografía para RTL
El árabe y el hebreo necesitan fuentes apropiadas — las fuentes del sistema por defecto pueden renderizarse de forma deficiente.
Fuentes recomendadas:
- Noto Sans Arabic — cobertura completa de Google, gratuita, combina con la familia Noto
- IBM Plex Arabic — excelente para herramientas técnicas/de desarrollador, combina con IBM Plex Sans
- Cairo — moderna, limpia, ideal para interfaces de usuario
- Tajawal — geométrica, combina bien con fuentes LTR sans-serif
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap");
:lang(ar) {
font-family: "Noto Sans Arabic", "Segoe UI", system-ui, sans-serif;
font-size: 1.05em; /* El árabe suele leerse mejor ligeramente más grande */
line-height: 1.8; /* El árabe necesita más espacio vertical que el latín */
}
:lang(he) {
font-family: "Noto Sans Hebrew", "Segoe UI", system-ui, sans-serif;
line-height: 1.6;
}
Ajustes tipográficos clave:
- El texto árabe generalmente se beneficia de un tamaño de fuente 1-2 px mayor en comparación con texto latino equivalente al mismo tamaño
- El interlineado debe ser de 1.6–2.0 para el árabe (frente a 1.4–1.6 para el latín) debido a las marcas diacríticas
- El espaciado entre letras (
letter-spacing) generalmente debe ser 0 o ligeramente negativo para el árabe — nunca positivo font-variant-numeric: ltrmantiene los numerales renderizándose de izquierda a derecha dentro del texto RTL
Texto bidireccional (Bidi)
Los documentos RTL a menudo contienen fragmentos LTR: nombres de marcas en inglés, números, URLs, código. El Algoritmo Bidireccional Unicode gestiona la mayoría de los casos automáticamente, pero las maquetaciones complejas necesitan un control explícito.
El elemento <bdi> aísla la direccionalidad de un fragmento del texto circundante:
<p dir="rtl"> المستخدم <bdi>JohnDoe123</bdi> أرسل رسالة </p>
Sin <bdi>, el nombre de usuario podría renderizarse con una direccionalidad incorrecta dependiendo de sus caracteres.
unicode-bidi: isolate hace lo mismo en CSS:
.username {
unicode-bidi: isolate;
}
Los números permanecen LTR en texto RTL automáticamente — el algoritmo Bidi lo gestiona. Los números de teléfono, precios y fechas se renderizan en orden de lectura. Normalmente no necesitas intervenir.
Los bloques de código siempre deben ser LTR:
pre, code {
direction: ltr;
unicode-bidi: isolate;
text-align: start; /* start = left en LTR */
}
Los nombres de marcas y productos escritos en caracteres latinos permanecerán naturalmente LTR dentro del texto RTL. Si ves un renderizado incorrecto, envuélvelos en <bdi> o en un span con dir="ltr":
<span dir="ltr">Better i18n</span>
Cuando gestionas traducciones a través de una plataforma como Better i18n, el SDK entrega las cadenas de locale ya incluyendo los marcadores de dirección en línea que tus traductores han añadido, por lo que no necesitas post-procesar las cadenas en el código de la aplicación.
RTL y Better i18n
Si estás gestionando traducciones entre locales LTR y RTL, los datos de dirección deben residir junto a la configuración del locale — no dispersos por los componentes. Better i18n proporciona metadatos del locale, incluida la dirección del texto, a través del mismo SDK que entrega tus cadenas de traducción. Cuando cambias a un usuario al árabe, la dirección cambia automáticamente como parte de la activación del locale en lugar de ser un paso adicional que alguien tiene que recordar configurar.
Lista de verificación de implementación RTL
Antes de publicar el soporte RTL, verifica:
CSS
- Cambiado el margen/padding/borde físico a propiedades lógicas
- Reemplazado el posicionamiento
left/rightconinset-inline-start/inset-inline-end - Reemplazado
text-align: left/rightcontext-align: start/end - Los iconos direccionales se invierten con el selector
[dir="rtl"]o la variantertl:de Tailwind - Las animaciones y transformaciones tienen en cuenta la dirección
HTML
- El atributo
direstablecido en el elemento<html> - El atributo
langestablecido correctamente -
<bdi>usado para contenido en línea de dirección mixta
React
- El proveedor de contexto de dirección envuelve la aplicación
- El mapeo de locale a dirección gestiona todos los locales RTL
- Los componentes que consumen la dirección usan el hook, no valores codificados
Pruebas
- Capturas de pantalla RTL revisadas para todas las páginas clave
- Las pruebas automatizadas cubren la maquetación RTL
- Lista de verificación de QA completada
Tipografía
- Fuentes apropiadas para RTL cargadas para árabe/hebreo/persa
- Interlineado ajustado para scripts RTL
- Tamaño de fuente modificado donde sea necesario
El soporte RTL no es una funcionalidad única — es una propiedad de todo el sistema de maquetación. Los equipos que lo hacen bien tratan la dirección como una dimensión de primera clase de su sistema de diseño, eligiendo propiedades lógicas desde el principio y dejando que flexbox y grid hagan el trabajo pesado. Los equipos que tienen dificultades son los que añadieron suposiciones LTR en cada rincón de su CSS y ahora necesitan sobrescribirlas una por una.
Empieza con las propiedades lógicas. Establece la dirección en el elemento HTML. Deja que flexbox gestione el resto automáticamente. Luego ocúpate de los patrones específicos — iconos, transformaciones, fuentes — que necesitan conciencia direccional. Hecho con cuidado, el soporte RTL es mucho menos trabajo del que la mayoría de los equipos esperan.
Better i18n es una plataforma de localización orientada al desarrollador, construida para equipos de frontend modernos. SDKs con tipado seguro, flujos de trabajo basados en Git, entrega por CDN y traducción con IA con aplicación de glosario — sin archivos de locale en tu repositorio.