Next.js

Guía RTL de Tailwind CSS: propiedades lógicas y la variante rtl:

Crea diseños RTL con Tailwind CSS 4 y next-intl: dir y lang por idioma, utilidades lógicas, la variante rtl:, fuentes árabes y un checklist de pruebas.

Por Aniq-UIActualizado el 05 de noviembre de 202625 min de lectura
Compartir:
Guía RTL de Tailwind CSS: propiedades lógicas y la variante rtl:
#Tailwind CSS#RTL#Arabic#Next.js#next-intl#i18n

Esta es una guía práctica de RTL con Tailwind CSS 4: cómo hacer que un mismo conjunto de componentes funcione en inglés, que se lee de izquierda a derecha, y en árabe, que se lee de derecha a izquierda, en un proyecto de Next.js App Router con next-intl. Los fragmentos de código vienen de las plantillas de Aniq UI, que se entregan en inglés y árabe, y cada clase de Tailwind citada se comprobó en la documentación y el compilador de Tailwind CSS 4.3.

La idea cabe en una frase: pon dir en <html>, escribe márgenes y posiciones con utilidades lógicas (ms-4, no ml-4) y reserva la variante rtl: para los pocos casos que las propiedades lógicas no pueden expresar. El resto de la página cubre lo que esa frase deja fuera: qué se invierte solo, qué necesita una variante, qué no debe invertirse nunca, qué cambia en la tipografía árabe y cómo probarlo todo.

1. Definir lang y dir en <html> para cada idioma

Todo lo demás en esta guía depende de un solo atributo. Las propiedades lógicas leen la dirección del elemento, la variante rtl: de Tailwind la busca y los lectores de pantalla usan lang para elegir una voz. Su sitio es <html>, no un contenedor dentro de <body>: los diálogos, menús y avisos suelen renderizarse en un portal justo debajo de <body>, y solo heredan la dirección si está definida por encima de ellos.

Con next-intl y el App Router, lo más limpio es que app/[locale]/layout.tsx renderice <html> y <body>, para que el servidor envíe la dirección correcta desde el primer byte de HTML. Los archivos siguientes vienen de la plantilla de landing page de Aniq UI, recortados a lo esencial. Primero el enrutado y el proxy (Next.js 16 renombró middleware.ts a proxy.ts; en Next.js 15 el mismo código va en middleware.ts):

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["en", "ar"],
  defaultLocale: "en",
});
// src/proxy.ts (named middleware.ts before Next.js 16)
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";

export default createMiddleware(routing);

export const config = {
  // Skip /api, /_next, /_vercel and any path with a dot (files)
  matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

La configuración de la petición carga los mensajes del idioma activo. La plantilla carga una carpeta por página; aquí es un archivo por idioma para abreviar:

// src/i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  };
});

Una pequeña función convierte un idioma en una dirección. Esta viene de la plantilla inmobiliaria de Aniq UI. Si más adelante añades hebreo, persa o urdu, es el único sitio que hay que cambiar:

// src/config/i18n.ts
export const directionOf = (locale: string): "rtl" | "ltr" =>
  locale === "ar" ? "rtl" : "ltr";

Después, el layout del idioma renderiza los atributos en el servidor:

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import { directionOf } from "@/config/i18n";
import "@/styles/globals.css";

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  if (!hasLocale(routing.locales, locale)) notFound();

  // Enable static rendering
  setRequestLocale(locale);
  const messages = await getMessages();

  return (
    <html lang={locale} dir={directionOf(locale)} suppressHydrationWarning>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

La plantilla conserva un layout raíz que solo pasa sus hijos, porque todas las páginas viven bajo [locale]:

// src/app/layout.tsx
// Every page lives under [locale], whose layout renders <html> and <body>.
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return children;
}

next.config envuelve la configuración con createNextIntlPlugin("./src/i18n/request.ts"). Para los detalles de Next.js 16 (root params, caché), consulta nuestra guía de next-intl y Next.js 16.

¿Por qué no definir dir en el navegador?

Algunas aplicaciones definen document.documentElement.dir en un useEffect. El HTML del servidor llega entonces de izquierda a derecha, la página se pinta así y se invierte tras la hidratación: un salto visible en cada carga de una página en árabe. Si tu <html> tiene que vivir en un layout raíz por encima del segmento [locale], la alternativa es un pequeño script en línea en <head> que lee el primer segmento de la ruta y define lang y dir antes del primer pintado. Las tiendas e-commerce y LMS de Aniq UI lo hacen así. Lo que hay que evitar es el efecto que se ejecuta después de la hidratación.

2. Utilidades lógicas en Tailwind CSS 4

Una utilidad física nombra un lado de la pantalla. ml-4 es margin-left en los dos idiomas, así que el espacio entre un icono y su etiqueta en inglés acaba en el lado equivocado en árabe. Una utilidad lógica nombra un lado del texto. ms-4 es margin-inline-start, que es la izquierda en inglés y la derecha en árabe: una clase, dos direcciones, sin sobrescrituras. Tailwind CSS las incluye desde la versión 3.3, y la versión 4 las amplía.

Física (lado fijo)Lógica (sigue dir)Propiedad CSS
ml-4 / mr-4ms-4 / me-4margin-inline-start / margin-inline-end
pl-4 / pr-4ps-4 / pe-4padding-inline-start / padding-inline-end
left-0 / right-0inset-s-0 / inset-e-0 (o start-0 / end-0)inset-inline-start / inset-inline-end
text-left / text-righttext-start / text-endtext-align: start / end
border-l / border-rborder-s / border-eborder-inline-start-width / border-inline-end-width
rounded-l-lg / rounded-r-lgrounded-s-lg / rounded-e-lgLas dos esquinas del lado de inicio o de fin
rounded-tl-lgrounded-ss-lgborder-start-start-radius
float-left / float-rightfloat-start / float-endfloat: inline-start / inline-end
scroll-ml-4scroll-ms-4scroll-margin-inline-start

¿start-* o inset-s-*? Tailwind CSS 4.2 añadió inset-s-*, inset-e-*, inset-bs-* e inset-be-*, y marcó start-* y end-* como obsoletos en su favor. Los nombres antiguos siguen compilando al mismo inset-inline-start (lo comprobamos con la 4.3.3) y las plantillas de Aniq UI todavía usan start-*, así que no hay prisa por reescribir. El código nuevo puede usar inset-s-*. La 4.2 también añadió utilidades del eje de bloque como mbs-*, pbs-* y border-bs-*; sirven para los modos de escritura vertical, no para el árabe, que es horizontal.

Lo que ya sigue la dirección

Buena parte del diseño se invierte sin ninguna clase nueva. Flexbox y grid colocan los elementos en el eje en línea, así que una fila flex empieza a la derecha en árabe, y justify-start, justify-end y order-* la siguen. mx-*, px-* e inset-x-* fijan los dos lados a la vez. En Tailwind CSS 4, space-x-* y divide-x también se escriben con propiedades lógicas. El valor por defecto de text-align es start, así que el texto que nunca alineaste ya está bien. Esto es lo que genera el compilador:

/* ms-4 */
.ms-4 {
  margin-inline-start: calc(var(--spacing) * 4);
}

/* space-x-4: logical in Tailwind CSS 4, so it follows the direction */
:where(.space-x-4 > :not(:last-child)) {
  --tw-space-x-reverse: 0;
  margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
  margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
}

/* translate-x is physical: -100% always means "towards the left" */
.-translate-x-full {
  --tw-translate-x: -100%;
  translate: var(--tw-translate-x) var(--tw-translate-y);
}

La regla práctica: nunca añadas flex-row-reverse ni rtl:flex-row-reverse para "arreglar" el árabe. La fila ya se invirtió cuando cambió la dirección, y volver a invertirla la deja en el orden del inglés. Y fíjate en el último bloque: translate-x es físico, por eso vuelve a aparecer en la siguiente sección.

Un campo con icono

El campo de texto de la plantilla inmobiliaria de Aniq UI muestra el patrón en un solo componente. El icono se coloca con start-4 y el texto recibe ps-11, así que en árabe los dos pasan a la derecha sin sobrescrituras:

// Input.tsx (trimmed): the icon sits at the inline start,
// and the text gets padding on the same side
<div className="relative">
  {startIcon && (
    <span className="pointer-events-none absolute start-4 top-1/2 -translate-y-1/2">
      {startIcon}
    </span>
  )}
  <input
    className={cn(
      "h-12",
      startIcon && "ps-11",
      // A left-to-right field (a phone number) on a right-to-left page
      // starts on the other side, so the icon's side needs the same room.
      startIcon && props.dir === "ltr" && "rtl:pe-11",
      className,
    )}
    {...props}
  />
</div>

La última condición resuelve un caso sutil que se trata en la sección 3: un campo de teléfono con dir="ltr" en una página en árabe.

Separadores y bordes

Las líneas entre elementos, los marcadores de la barra lateral y el borde de una columna del pie de página son donde suele esconderse border-l. La misma plantilla usa border-s en todas partes:

// A row of facts: a rule before every item except the first
<div className="flex flex-col items-center gap-1.5 border-s border-[var(--border)] px-2 first:border-s-0">

// A footer column: rule and padding on the side the text starts from
<div className="xl:border-s xl:border-[var(--border)] xl:ps-10 xl:pe-11">

Cuándo fallan las utilidades físicas, y cuándo están bien

Estas son las clases físicas que más a menudo se ven bien en inglés y fallan en árabe:

  • ml-auto para empujar un botón al final de una fila. Usa ms-auto.
  • pl-10 para dejar sitio a un icono de búsqueda. Usa ps-10, con el icono en start-3 o inset-s-3.
  • left-0 o right-1 para una insignia o un botón de cerrar. Usa las versiones lógicas de inset.
  • text-left en celdas de tabla y etiquetas de formulario. Usa text-start, o nada.
  • rounded-l-md en el primer botón de un grupo. Usa rounded-s-md.
  • border-l-2 como marcador activo de una barra lateral. Usa border-s-2.

Las utilidades físicas siguen siendo correctas cuando lo que colocan es físico. left-1/2 -translate-x-1/2 centra un elemento en las dos direcciones. inset-0 e inset-x-0 son simétricas. El deslizador de antes y después de la plantilla inmobiliaria mantiene la etiqueta "Original" en left-4 y "Amueblado" en right-4 en los dos idiomas porque, como dice su comentario, el deslizador es un antes y después físico, no un orden de lectura. Decide elemento por elemento y escribe el motivo en un comentario para que la siguiente persona no lo "arregle".

3. Las variantes rtl: y ltr:

Las propiedades lógicas cubren márgenes, posiciones, bordes, radios y alineación. No cubren transformaciones, degradados, orígenes de transformación, keyframes ni el significado de un icono. Para eso, Tailwind tiene rtl: y ltr:. Esto es lo que generan dos de ellas en Tailwind CSS 4.3:

/* rtl:-scale-x-100 */
.rtl\:-scale-x-100:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) {
  --tw-scale-x: calc(100% * -1);
  scale: var(--tw-scale-x) var(--tw-scale-y);
}

/* ltr:-translate-x-full */
.ltr\:-translate-x-full:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *) {
  --tw-translate-x: -100%;
  translate: var(--tw-translate-x) var(--tw-translate-y);
}

Hay dos detalles importantes. El selector va dentro de :where(), así que no añade especificidad: una clase rtl: gana a la clase base solo porque se emite más tarde en la hoja de estilos (y las reglas ltr: van antes que las rtl:). Y coincide de tres maneras: la dirección calculada del propio elemento, un atributo dir="rtl" en el elemento o cualquier antecesor con dir="rtl". Ten en cuenta esto último para las islas de más abajo.

Iconos direccionales

Una flecha que significa "siguiente" apunta a la derecha en inglés y a la izquierda en árabe. Dos plantillas de Aniq UI muestran las dos formas habituales de girarla:

// An arrow at the end of a button: mirrored in Arabic
endIcon={<IconArrowRight aria-hidden className="h-5 w-5 rtl:-scale-x-100" />}

// A back link: the same result with a rotation
<IconArrowLeft size={16} className="me-1 rtl:rotate-180" />

rtl:-scale-x-100 es una imagen en espejo real. rtl:rotate-180 además pone el icono boca abajo, lo que no se nota en una flecha o un chevron simétricos pero queda mal en todo lo que tiene arriba y abajo (una flecha curva de "responder", un icono con cola). Ante la duda, mejor el espejo.

Gira las flechas de atrás y adelante, los chevrons de las migas de pan y la paginación, los enlaces "ver todo" y todo lo que muestre un movimiento hacia delante. Las pautas de bidireccionalidad de Material Design no invierten los botones de reproducción ni la barra de progreso de un medio, porque indican la dirección de la cinta y no la de lectura, y dejan igual los relojes y los iconos de actualizar en sentido horario, porque los relojes siguen girando en sentido horario. Los logotipos no se invierten nunca.

Transformaciones y translate-x

translate-x es físico, así que todo lo que se desliza fuera de la pantalla necesita una variante. El menú móvil de la plantilla LMS de Aniq UI se coloca con utilidades lógicas (start-0, rounded-e-xl) y se oculta con una traslación que tiene en cuenta la dirección:

// A mobile menu that slides in from the inline start
<div
  className={cn(
    "absolute top-0 start-0 h-full w-64 rounded-e-xl transition-transform duration-300",
    isOpen ? "translate-x-0" : "ltr:-translate-x-full rtl:translate-x-full",
  )}
>

El centrado es el otro caso habitual. Cuando un elemento se coloca en start-1/2, su desplazamiento de media anchura tiene que cambiar de signo en árabe. Cuando se coloca en left-1/2, no hace falta ninguna variante. Las dos líneas siguientes vienen de las plantillas de restaurante y e-commerce de Aniq UI:

// start-1/2 moves with the direction, so the half-width shift has to as well
"absolute -top-12 start-1/2 h-10 w-10 ltr:-translate-x-1/2 rtl:translate-x-1/2 rounded-full"

// left-1/2 with -translate-x-1/2 centres in both directions, no variant needed
"absolute bottom-6 left-1/2 -translate-x-1/2"

Dirección de los degradados

Tailwind CSS 4.3 no tiene una dirección de degradado lógica: las utilidades documentadas son bg-linear-to-r, bg-linear-to-l y los demás lados físicos, además de ángulos como bg-linear-65, y ninguna se invierte. Un velo detrás de un texto necesita la variante. La plantilla de e-commerce usa los nombres antiguos bg-gradient-to-*, que siguen compilando en la versión 4:

// A banner scrim: dark behind the text, transparent towards the picture
<div className="absolute inset-0 bg-gradient-to-r from-black/85 via-black/45 to-transparent rtl:bg-gradient-to-l" />

// The same with the Tailwind CSS 4 names
<div className="absolute inset-0 bg-linear-to-r from-black/85 to-transparent rtl:bg-linear-to-l" />

Origen de la transformación

origin-left también es físico. Una barra de progreso que crece con scale-x debe crecer desde el lado en el que empieza el texto, como en la galería de platos de la plantilla de restaurante:

// A progress bar that grows from the side the text starts on
"absolute inset-x-0 bottom-0 h-1 bg-[var(--primaryColor)] ltr:origin-left rtl:origin-right"

Animaciones de entrada lateral

Los keyframes no pueden leer una clase de utilidad, así que un panel que entra deslizándose necesita un keyframe por dirección. El panel lateral de la plantilla inmobiliaria está en inset-y-0 end-0 y cambia el nombre de la animación bajo [dir="rtl"]:

/* A sheet enters from the inline end: the right in English, the left in Arabic */
.animate-sheet-in {
  animation: velra-sheet-in-ltr 240ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

[dir="rtl"] .animate-sheet-in {
  animation-name: velra-sheet-in-rtl;
}

@keyframes velra-sheet-in-ltr {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

@keyframes velra-sheet-in-rtl {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

Flechas del teclado

La dirección también es comportamiento. En una lista de pestañas o un deslizador, la flecha derecha del teclado debe ir al siguiente elemento en pantalla, que en árabe está a la izquierda. Las pestañas del panel de restaurante intercambian las teclas:

// Arrow keys follow what the user sees, not the DOM order
const isRtl = useLocale() === "ar";
const forward = vertical ? "ArrowDown" : isRtl ? "ArrowLeft" : "ArrowRight";
const backward = vertical ? "ArrowUp" : isRtl ? "ArrowRight" : "ArrowLeft";

El truco: rtl: sigue aplicándose dentro de una isla dir="ltr"

Como rtl: coincide con cualquier elemento que tenga un antecesor dir="rtl", sigue aplicándose dentro de un elemento dir="ltr" en una página en árabe. En ese elemento coinciden ltr: (su propia dirección) y rtl: (su antecesor), y gana la regla rtl: porque va después. A veces es justo lo que quieres: el campo de teléfono del fragmento anterior es dir="ltr", así que su ps-11 deja espacio a la izquierda, mientras que el icono está a la derecha de la página en árabe, y rtl:pe-11 deja también espacio a la derecha. Cuando necesites la dirección propia del elemento, usa una variante arbitraria basada en la pseudoclase :dir(), que todos los navegadores actuales admiten:

// Matches when this element itself is right to left,
// not when any ancestor has dir="rtl"
<IconChevronRight className="[&:dir(rtl)]:-scale-x-100" />

4. Lo que no debe invertirse

El algoritmo bidireccional del navegador ya muestra de izquierda a derecha las palabras latinas y los dígitos dentro de un texto en árabe. El problema aparece con secuencias que mezclan dígitos y signos: un teléfono como +1 555 010 2030, un precio con código de moneda, un rango o un contador como 3 / 12. Sus espacios y símbolos toman la dirección del árabe que los rodea, y partes de la secuencia pueden salir en el orden equivocado. La solución es aislar la secuencia como una isla propia de izquierda a derecha.

Números y dígitos latinos

Muchas interfaces en árabe, entre ellas las plantillas de Aniq UI citadas aquí, usan dígitos latinos (0 a 9) en árabe. Que Intl los produzca depende de la etiqueta de idioma y de los datos ICU del navegador, así que pídelos de forma explícita con la extensión -u-nu-latn. Este es el formateador del panel de restaurante:

// Figures are Latin in every locale, so Arabic asks for Latin numbering explicitly.
const resolveNumberLocale = (locale: string): string =>
  locale === "ar" ? "ar-u-nu-latn" : locale || "en";

export const formatNumber = (
  value: number,
  locale: string,
  options?: Intl.NumberFormatOptions,
): string => new Intl.NumberFormat(resolveNumberLocale(locale), options).format(value);

En Node.js 24 (ICU 78), la región decide los dígitos y la extensión manda sobre ella:

new Intl.NumberFormat("ar-SA").format(1248.6);           // "١٬٢٤٨٫٦" (Arabic-Indic digits)
new Intl.NumberFormat("ar-SA-u-nu-latn").format(1248.6); // "1,248.6"
new Intl.NumberFormat("ar-u-nu-latn").format(1248.6);    // "1,248.6"

Usa la misma etiqueta para fechas, horas y monedas, y ten en cuenta que Intl añade marcas invisibles de derecha a izquierda (U+200F) alrededor de las monedas y fechas en árabe. Tenlo presente al comparar cadenas formateadas en tus pruebas.

Precios

La plantilla inmobiliaria envuelve cada precio en <bdi dir="ltr">, lo que lo aísla del árabe que lo rodea y fija su orden. tabular-nums da a todos los dígitos la misma anchura para que las cifras queden alineadas en las listas:

// Every price on the site: "AED 4,850,000" keeps its order inside Arabic text
<bdi dir="ltr" className="tabular-nums">
  {formatPrice(price, currency)}
</bdi>

Teléfonos, correos y URL

Los números de teléfono, los correos, las URL, los códigos de idioma y los contadores van siempre de izquierda a derecha. La misma plantilla los marca uno a uno y da al campo de teléfono rtl:text-end, para que el número se escriba de izquierda a derecha pero quede alineado con las etiquetas en árabe:

// A phone field: typed left to right, aligned with the Arabic form
<Input
  label={t("phone")}
  startIcon={<IconPhone aria-hidden className="h-5 w-5" />}
  type="tel"
  autoComplete="tel"
  dir="ltr"
  className="rtl:text-end"
  {...register("phone")}
/>

// An email address
<a href={`mailto:${email}`} dir="ltr" className="block truncate">{email}</a>

// The language switcher's "EN" and "AR"
<span dir="ltr">{locale.toUpperCase()}</span>

// A gallery counter such as "3 / 12"
<p aria-live="polite" dir="ltr">
  {t("counter", { index: String(index + 1), total: String(images.length) })}
</p>

Código y logotipos

El código siempre va de izquierda a derecha. En contenido en árabe, renderiza <pre> y el <code> en línea con dir="ltr" (o direction: ltr en la hoja de estilos que muestra tu contenido). Con un logotipo latino pasa lo mismo: el nombre de marca de la plantilla inmobiliaria es un span con dir="ltr", así que sus letras y su espaciado no se mueven nunca. Las fotos y las ilustraciones tampoco se invierten, porque una foto girada muestra texto en espejo y un producto al revés.

// The brand name is a Latin word mark: it never mirrors
<span dir="ltr" className="font-semibold tracking-[-0.02em]">
  {BRAND_CONFIG.name.en}
</span>

Gráficos y ejes de tiempo

Los gráficos son una decisión de diseño. Algunos productos en árabe mantienen el tiempo de izquierda a derecha, como los números del eje; otros lo llevan de derecha a izquierda para seguir la lectura. Las dos opciones son defendibles; lo importante es que la librería de gráficos mantenga un sistema de coordenadas estable de izquierda a derecha. El panel de restaurante de Aniq UI pone el gráfico en un contenedor dir="ltr" y después invierte a propósito el eje del tiempo en árabe, con el eje de valores a la derecha:

const isRtl = locale === "ar";

<div dir="ltr" role="img" aria-label={label} className="relative w-full flex-1">
  <ResponsiveContainer width="100%" height="100%">
    <AreaChart data={data}>
      <XAxis dataKey="label" reversed={isRtl} />
      <YAxis orientation={isRtl ? "right" : "left"} />
      {/* grid, tooltip and area */}
    </AreaChart>
  </ResponsiveContainer>
</div>

Panel de administración de restaurante de Aniq UI en árabe: barra lateral a la derecha, gráfico de ingresos con el tiempo de derecha a izquierda y el eje de valores a la derecha, cifras latinas

Islas dir="ltr", <bdi>, dir="auto" y unicode-bidi

  • dir="ltr" en un elemento crea una isla de izquierda a derecha. Los navegadores ya aíslan del texto vecino un elemento con atributo dir (la hoja de estilos por defecto de HTML le da unicode-bidi: isolate), así que rara vez hace falta más.
  • <bdi> aísla una secuencia en línea cuya dirección quizá no conozcas, como un nombre de usuario dentro de una frase. Añade dir="ltr" cuando sí la conozcas, como con los precios.
  • dir="auto" deja que el navegador elija la dirección según el primer carácter fuerte. Úsalo para el texto que escriben los usuarios, que puede estar en cualquiera de los dos idiomas:
// Text written by users can be in either language: let the browser decide
<p dir="auto">{review.body}</p>

Tailwind CSS no tiene una utilidad unicode-bidi, y la clase isolate es otra cosa (isolation: isolate, que crea un contexto de apilamiento). Podrías escribir una propiedad arbitraria como [unicode-bidi:isolate], pero MDN advierte a los autores que no sobrescriban unicode-bidi, y el atributo dir o <bdi> hacen el mismo trabajo en el marcado.

5. Fuentes árabes, interlineado y espaciado entre letras

Cargar una fuente árabe

Muchas fuentes de interfaz latinas no tienen glifos árabes, así que el navegador recurre a una fuente del sistema y las páginas en árabe parecen otro producto. Carga una fuente para cada escritura. La plantilla de restaurante combina Inter e IBM Plex Sans Arabic con next/font:

// src/app/layout.tsx (trimmed)
import { Inter, IBM_Plex_Sans_Arabic } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  weight: "variable",
  display: "swap",
  variable: "--font-inter",
});

const plexArabic = IBM_Plex_Sans_Arabic({
  subsets: ["arabic"],
  weight: ["300", "400", "500", "600", "700"],
  display: "swap",
  variable: "--font-arabic",
});

// <html className={`${inter.variable} ${plexArabic.variable}`}>

Después cambia la familia bajo [dir="rtl"]. La fuente árabe va primero e Inter después, así que las palabras latinas dentro de un texto en árabe (precios, nombres de marca, correos) recurren a Inter y se ven igual que en las páginas en inglés:

/* src/styles/base.css */
[dir="rtl"] body,
[dir="rtl"] {
  font-family: var(--font-arabic), var(--font-inter), Arial, ui-sans-serif, system-ui, sans-serif;
  letter-spacing: 0;
}

La plantilla inmobiliaria hace lo mismo con archivos de Noto Sans Arabic y Manrope alojados en el propio sitio, cada uno declarado con un unicode-range, de modo que una página solo descarga una fuente si tiene texto en esa escritura. Un selector sobre [dir="rtl"] evita añadir una clase a cada componente.

Interlineado

Las letras árabes tienen ascendentes altos y descendentes profundos, y los signos diacríticos van encima y debajo. Un interlineado pensado para un titular latino (1.1 es habitual) puede hacer que las líneas en árabe se toquen o se corten. El panel de restaurante abre sus títulos de página solo en árabe:

// A page title: tight in English, more room for Arabic
<h1 className="text-[28px] font-extrabold leading-[1.1] tracking-[-0.02em] lg:text-[36px] rtl:leading-[1.4]">

// An eyebrow label: wide tracking in English, none in Arabic
"text-[13px] font-semibold uppercase tracking-[0.16em] rtl:tracking-normal"

Espaciado entre letras

El árabe es una escritura ligada, y la nota de MDN sobre letter-spacing lo dice claramente: el espaciado aplicado al árabe puede hacer que el texto parezca roto. Es habitual apretar el espaciado en los titulares latinos y abrirlo en las etiquetas pequeñas en mayúsculas, así que restablécelo con rtl:tracking-normal, como en la línea de la etiqueta de arriba (de la plantilla inmobiliaria), o con letter-spacing: 0 en la regla [dir="rtl"]. El árabe tampoco tiene mayúsculas, así que uppercase no hace nada: una etiqueta que en inglés se apoya en las mayúsculas y el espaciado necesita peso o color para destacar en árabe.

6. Checklist de pruebas RTL

Pásala en cada página en árabe, con tema claro y oscuro, en anchura de móvil y de escritorio:

  1. El atributo está en el HTML del servidor. Mira el código fuente de la página (no el inspector) y busca <html lang="ar" dir="rtl">. Recarga con una red lenta y observa si hay un giro después de la carga.
  2. El diseño se refleja. La barra lateral, el logotipo, la navegación principal y la acción principal están a la derecha. Nada se invierte dos veces.
  3. Paneles, menús y avisos se abren desde el lado correcto y se muestran de derecha a izquierda, también los que van en portales.
  4. Flechas y chevrons apuntan bien en migas de pan, paginación, carruseles, botones de "siguiente" y enlaces de "ver todo". Los botones de reproducción y los relojes no se invierten.
  5. Campos. Los iconos, los botones de borrar y los iconos de validación están a la derecha. Los campos de teléfono, correo, URL y código se escriben de izquierda a derecha.
  6. Números. Los dígitos son los que elegiste, los precios y teléfonos se leen en el orden correcto y los contadores como 3 / 12 no se desordenan.
  7. Texto mixto. Un nombre de marca en inglés dentro de una frase en árabe mantiene la puntuación en su sitio.
  8. Tipografía. La fuente árabe se carga (DevTools muestra la fuente renderizada), los títulos no se cortan y ningún texto en árabe tiene espaciado entre letras.
  9. Teclado. Las flechas en pestañas, deslizadores y carruseles se mueven como se lee la pantalla.
  10. Gráficos: la convención elegida es la misma en todos.
  11. Clases físicas. Busca en el código las utilidades de izquierda y derecha y decide en cada resultado:
# List physical left/right classes in your components.
# Each hit is a question to answer, not always a bug.
grep -rnE --include='*.tsx' \
  '(^|[^a-z0-9-])-?(ml|mr|pl|pr|left|right)-([0-9]|\[|\(|px|auto|full)|(^|[^a-z0-9-])(rounded|border)-(l|r|tl|tr|bl|br)([^a-z0-9]|-|$)|(^|[^a-z0-9-])(text|float)-(left|right)([^a-z0-9-]|$)' \
  src

En las aplicaciones de Aniq UI donde lo ejecutamos devuelve entre 2 y 10 líneas por aplicación: elementos centrados, pares simétricos left-0 right-0, un deslizador de antes y después físico y líneas que ya tienen una sobrescritura rtl:. Ese es el tamaño de lista que buscas: lo bastante corta como para leer cada línea.

Preguntas frecuentes

¿Tailwind CSS admite RTL?

Sí. Tailwind CSS tiene utilidades lógicas como ms-*, pe-*, inset-s-*, text-start, border-s y rounded-e-*, que siguen el atributo dir, además de las variantes rtl: y ltr: para los estilos que deben cambiar según la dirección. Las dos vienen integradas.

¿Necesito un plugin RTL con Tailwind CSS 4?

No. Las utilidades lógicas forman parte de Tailwind CSS desde la versión 3.3, y las variantes rtl: y ltr: están en el núcleo. Define dir en <html> y úsalas.

¿Debo usar start-* o inset-s-*?

Tailwind CSS 4.2 marcó start-* y end-* como obsoletos en favor de inset-s-* e inset-e-*. Los dos compilan a inset-inline-start e inset-inline-end, así que el código existente sigue funcionando. Usa inset-s-* en el código nuevo si estás en la 4.2 o posterior.

¿Utilidades lógicas o la variante rtl:?

Primero las utilidades lógicas. ms-3 sustituye a ltr:ml-3 rtl:mr-3 con una sola clase que no puede desincronizarse. Reserva rtl: para lo que las propiedades lógicas no pueden expresar: iconos en espejo, translate-x, dirección de los degradados, orígenes de transformación y ajustes tipográficos como el interlineado y el espaciado entre letras.

¿Cómo giro un icono en RTL con Tailwind?

Añade rtl:-scale-x-100 al icono, que lo refleja en horizontal. rtl:rotate-180 también sirve para flechas y chevrons simétricos. Gira solo los iconos que indican dirección; los controles de medios, los relojes y los logotipos se quedan como están.

¿Cómo mantengo los dígitos latinos en árabe?

Formatea los números con una etiqueta de idioma que incluya -u-nu-latn, por ejemplo new Intl.NumberFormat("ar-u-nu-latn") o "ar-SA-u-nu-latn". Usa la misma etiqueta para fechas y monedas.

¿Basta con dir="rtl" en <body> o en un div contenedor?

Ponlo en <html>. El contenido renderizado en portales, como diálogos y avisos, queda fuera de tu contenedor y solo hereda una dirección definida en <html> o <body>, y un atributo en la raíz da además a los lectores de pantalla y al selector :dir() una única fuente de verdad.

Plantillas con RTL integrado

Cada patrón de esta guía viene de código que se entrega en las plantillas de Aniq UI. Todas son Next.js con Tailwind CSS 4 y next-intl, en inglés y árabe con un diseño de derecha a izquierda real y demos en vivo que puedes cambiar al árabe. Si prefieres partir de una base donde este trabajo ya está hecho, las nueve plantillas de dashboard en árabe RTL están comparadas una junto a otra, y el resto (restaurante, inmobiliaria, LMS, e-commerce y landing pages) está en la página de plantillas.

Lo que cuesta cada una, en un solo pago, con el código fuente y demos en vivo que puedes cambiar al árabe:

  • Sitios web y tiendas (restaurante, inmobiliaria, LMS, e-commerce, alquiler vacacional, venta de entradas): desde $39.
  • Landing pages (SaaS y empresa): $39.
  • Dashboards de administración: desde $49. Los 9 comparados.
  • Full stack (sitio, dashboard y API juntos): $149.
  • Las 10 plantillas: el paquete, por una fracción de los precios por separado.

Explora las plantillas y abre cualquier demo en árabe antes de comprar.

Fuentes

Revisado en noviembre de 2026.

¿Encontró útil este artículo?

Compartir:
Boletín

Recibe un aviso cuando salga una nueva plantilla

Sé el primero en saber cuándo se publica una nueva plantilla.

  • Nuevas plantillas publicadas
  • Mira lo que se está creando y vota
  • Lanzamientos gratuitos y premium
  • Cancela la suscripción con un clic
world map

Comunidad Global de Usuarios

Únete a miles de desarrolladores en todo el mundo que confían en Aniq-UI para sus proyectos. Nuestras plantillas se utilizan en todo el mundo para crear experiencias web impresionantes.

Aniq-uiAniq-ui

Preferencias de Cookies

Utilizamos cookies para mejorar tu experiencia de navegación, analizar el tráfico del sitio y personalizar el contenido. Al hacer clic en "Aceptar Todo", consientes nuestro uso de cookies para análisis y publicidad personalizada.