Guide RTL Tailwind CSS : propriétés logiques et variante rtl:
Créez des mises en page RTL avec Tailwind CSS 4 et next-intl : dir et lang par langue, utilitaires logiques, variante rtl:, polices arabes et checklist de test.

Ce guide pratique explique le RTL avec Tailwind CSS 4 : comment faire fonctionner un même ensemble de composants en anglais, qui se lit de gauche à droite, et en arabe, qui se lit de droite à gauche, dans un projet Next.js App Router avec next-intl. Les extraits de code viennent des templates d’Aniq UI, livrés en anglais et en arabe, et chaque classe Tailwind citée a été vérifiée dans la documentation et le compilateur de Tailwind CSS 4.3.
L’idée tient en une phrase : placer dir sur <html>, écrire les marges et les positions avec des utilitaires logiques (ms-4, pas ml-4), et garder la variante rtl: pour les rares cas que les propriétés logiques ne savent pas exprimer. Le reste de la page couvre ce que cette phrase laisse de côté : ce qui s’inverse tout seul, ce qui demande une variante, ce qui ne doit jamais s’inverser, ce qui change pour la typographie arabe et comment tout tester.
1. Définir lang et dir sur <html> pour chaque langue
Tout le reste du guide dépend d’un seul attribut. Les propriétés logiques lisent la direction de l’élément, la variante rtl: de Tailwind la cherche, et les lecteurs d’écran utilisent lang pour choisir une voix. Sa place est sur <html>, pas sur un conteneur dans <body> : les boîtes de dialogue, menus et notifications sont souvent rendus dans un portail directement sous <body>, et ils n’héritent de la direction que si elle est définie au-dessus d’eux.
Avec next-intl et l’App Router, le plus propre est de laisser app/[locale]/layout.tsx rendre <html> et <body>, pour que le serveur envoie la bonne direction dès le premier octet de HTML. Les fichiers ci-dessous viennent du template de landing page d’Aniq UI, réduits à l’essentiel. D’abord le routage et le proxy (Next.js 16 a renommé middleware.ts en proxy.ts ; sous Next.js 15, le même code va dans 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 configuration de requête charge les messages de la langue active. Le template charge un dossier par page ; ici, un fichier par langue pour rester court :
// 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,
};
});
Une petite fonction transforme une langue en direction. Celle-ci vient du template immobilier d’Aniq UI. Si vous ajoutez plus tard l’hébreu, le persan ou l’ourdou, c’est le seul endroit à modifier :
// src/config/i18n.ts
export const directionOf = (locale: string): "rtl" | "ltr" =>
locale === "ar" ? "rtl" : "ltr";
Le layout de langue rend ensuite les attributs côté serveur :
// 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>
);
}
Le template garde un layout racine qui se contente de transmettre ses enfants, puisque toutes les pages vivent sous [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 enveloppe la configuration avec createNextIntlPlugin("./src/i18n/request.ts"). Pour les détails propres à Next.js 16 (root params, cache), voyez notre guide next-intl et Next.js 16.
Pourquoi ne pas définir dir dans le navigateur ?
Certaines applications définissent document.documentElement.dir dans un useEffect. Le HTML du serveur arrive alors de gauche à droite, la page s’affiche ainsi, puis s’inverse après l’hydratation : un saut visible à chaque chargement d’une page arabe. Si votre <html> doit rester dans un layout racine au-dessus du segment [locale], la solution de repli est un petit script en ligne dans <head> qui lit le premier segment du chemin et définit lang et dir avant le premier affichage. Les boutiques e-commerce et LMS d’Aniq UI font ainsi. C’est l’effet exécuté après l’hydratation qu’il faut éviter.
2. Les utilitaires logiques de Tailwind CSS 4
Un utilitaire physique désigne un côté de l’écran. ml-4 vaut margin-left dans les deux langues, donc l’espace entre une icône et son libellé en anglais se retrouve du mauvais côté en arabe. Un utilitaire logique désigne un côté du texte. ms-4 vaut margin-inline-start, c’est-à-dire la gauche en anglais et la droite en arabe : une classe, deux directions, aucune surcharge. Tailwind CSS les propose depuis la version 3.3, et la version 4 les complète.
| Physique (côté fixe) | Logique (suit dir) | Propriété CSS |
|---|---|---|
ml-4 / mr-4 | ms-4 / me-4 | margin-inline-start / margin-inline-end |
pl-4 / pr-4 | ps-4 / pe-4 | padding-inline-start / padding-inline-end |
left-0 / right-0 | inset-s-0 / inset-e-0 (ou start-0 / end-0) | inset-inline-start / inset-inline-end |
text-left / text-right | text-start / text-end | text-align: start / end |
border-l / border-r | border-s / border-e | border-inline-start-width / border-inline-end-width |
rounded-l-lg / rounded-r-lg | rounded-s-lg / rounded-e-lg | Les deux coins du côté début ou fin |
rounded-tl-lg | rounded-ss-lg | border-start-start-radius |
float-left / float-right | float-start / float-end | float: inline-start / inline-end |
scroll-ml-4 | scroll-ms-4 | scroll-margin-inline-start |
start-* ou inset-s-* ? Tailwind CSS 4.2 a ajouté inset-s-*, inset-e-*, inset-bs-* et inset-be-*, et a déprécié start-* et end-* à leur profit. Les anciens noms compilent toujours vers le même inset-inline-start (vérifié avec la 4.3.3), et les templates Aniq UI utilisent encore start-* : rien ne presse. Le nouveau code peut utiliser inset-s-*. La 4.2 a aussi ajouté des utilitaires sur l’axe de bloc comme mbs-*, pbs-* et border-bs-* ; ils servent aux modes d’écriture verticaux, pas à l’arabe, qui s’écrit à l’horizontale.
Ce qui suit déjà la direction
Une bonne partie de la mise en page s’inverse sans aucune nouvelle classe. Flexbox et grid placent les éléments sur l’axe en ligne, donc une rangée flex commence à droite en arabe, et justify-start, justify-end et order-* suivent. mx-*, px-* et inset-x-* règlent les deux côtés à la fois. Dans Tailwind CSS 4, space-x-* et divide-x sont eux aussi écrits avec des propriétés logiques. La valeur par défaut de text-align est start, donc un texte que vous n’avez jamais aligné est déjà correct. Voici ce que produit le compilateur :
/* 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 règle pratique : n’ajoutez jamais flex-row-reverse ou rtl:flex-row-reverse pour « corriger » l’arabe. La rangée s’est déjà inversée quand la direction a changé, et l’inverser à nouveau la remet dans l’ordre anglais. Notez aussi le dernier bloc : translate-x est physique, c’est pourquoi il revient dans la section suivante.
Un champ avec une icône
Le champ de texte du template immobilier d’Aniq UI montre le principe dans un seul composant. L’icône est placée avec start-4 et le texte reçoit ps-11, donc en arabe les deux passent à droite sans surcharge :
// 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 dernière condition gère un cas subtil traité en section 3 : un champ de téléphone en dir="ltr" sur une page arabe.
Séparateurs et bordures
Les filets entre éléments, les marqueurs de barre latérale et le bord d’une colonne de pied de page sont les endroits où se cache souvent border-l. Le même template utilise border-s partout :
// 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">
Quand les utilitaires physiques cassent, et quand ils conviennent
Voici les classes physiques qui paraissent correctes en anglais et cassent le plus souvent en arabe :
ml-autopour pousser un bouton au bout d’une rangée. Utilisezms-auto.pl-10pour laisser la place à une icône de recherche. Utilisezps-10, avec l’icône enstart-3ouinset-s-3.left-0ouright-1pour un badge ou un bouton de fermeture. Utilisez les versions logiques de inset.text-leftsur les cellules de tableau et les libellés de formulaire. Utiliseztext-start, ou rien.rounded-l-mdsur le premier bouton d’un groupe. Utilisezrounded-s-md.border-l-2comme marqueur actif d’une barre latérale. Utilisezborder-s-2.
Les utilitaires physiques restent justes quand ce qu’ils placent est physique. left-1/2 -translate-x-1/2 centre un élément dans les deux directions. inset-0 et inset-x-0 sont symétriques. Le curseur avant/après photo du template immobilier garde son étiquette « Original » en left-4 et « Meublé » en right-4 dans les deux langues, parce que, comme le dit son commentaire, le curseur est un avant/après physique, pas un ordre de lecture. Décidez élément par élément, et écrivez la raison en commentaire pour que la personne suivante ne le « corrige » pas.
3. Les variantes rtl: et ltr:
Les propriétés logiques couvrent marges, positions, bordures, arrondis et alignement. Elles ne couvrent pas les transformations, les dégradés, les origines de transformation, les keyframes ni le sens d’une icône. Pour cela, Tailwind propose rtl: et ltr:. Voici ce que deux d’entre elles produisent avec 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);
}
Deux détails comptent. Le sélecteur est enveloppé dans :where(), il n’ajoute donc aucune spécificité : une classe rtl: l’emporte sur la classe de base uniquement parce qu’elle est émise plus loin dans la feuille de style (et les règles ltr: viennent avant les règles rtl:). Et il correspond de trois façons : la direction calculée de l’élément lui-même, un attribut dir="rtl" sur l’élément, ou n’importe quel ancêtre avec dir="rtl". Gardez ce dernier cas en tête pour les îlots plus bas.
Icônes directionnelles
Une flèche qui signifie « suivant » pointe à droite en anglais et à gauche en arabe. Deux templates Aniq UI montrent les deux façons habituelles de la retourner :
// 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 donne une vraie image miroir. rtl:rotate-180 retourne aussi l’icône tête en bas, ce qui ne se voit pas sur une flèche ou un chevron symétrique mais devient faux pour tout ce qui a un haut et un bas (une flèche « répondre » courbée, une icône avec une queue). Dans le doute, préférez le miroir.
Retournez les flèches précédent et suivant, les chevrons des fils d’Ariane et de la pagination, les liens « voir tout » et tout ce qui montre un mouvement vers l’avant. Les règles de bidirectionnalité de Material Design gardent les boutons de lecture et la barre de progression d’un média non inversés, car ils indiquent le sens de la bande, pas celui de la lecture, et laissent les horloges et les icônes d’actualisation dans le sens horaire telles quelles, car les horloges tournent toujours dans le sens horaire. Les logos ne s’inversent jamais.
Transformations et translate-x
translate-x est physique, donc tout ce qui glisse hors de l’écran a besoin d’une variante. Le menu mobile du template LMS d’Aniq UI est placé avec des utilitaires logiques (start-0, rounded-e-xl) et se masque avec une translation qui tient compte de la direction :
// 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",
)}
>
Le centrage est l’autre cas fréquent. Quand un élément est placé en start-1/2, son décalage d’une demi-largeur doit changer de signe en arabe. Quand il est placé en left-1/2, aucune variante n’est nécessaire. Les deux lignes ci-dessous viennent des templates restaurant et e-commerce d’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"
Sens des dégradés
Tailwind CSS 4.3 n’a pas de direction de dégradé logique : les utilitaires documentés sont bg-linear-to-r, bg-linear-to-l et les autres côtés physiques, plus des angles comme bg-linear-65, et aucun ne s’inverse. Un voile derrière du texte a donc besoin de la variante. Le template e-commerce utilise les anciens noms bg-gradient-to-*, qui compilent encore en version 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" />
Origine de transformation
origin-left est physique lui aussi. Une barre de progression qui grandit avec scale-x doit partir du côté où le texte commence, comme dans la galerie de plats du template restaurant :
// 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"
Animations d’entrée latérale
Les keyframes ne peuvent pas lire une classe utilitaire, donc un panneau qui glisse à l’écran a besoin d’une keyframe par direction. Le panneau latéral du template immobilier est placé en inset-y-0 end-0 et change de nom d’animation sous [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); }
}
Flèches du clavier
La direction est aussi un comportement. Dans une liste d’onglets ou un curseur, la flèche droite du clavier doit aller vers l’élément suivant à l’écran, qui est à gauche en arabe. Les onglets du dashboard restaurant inversent les touches :
// 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";
Le piège : rtl: s’applique encore dans un îlot dir="ltr"
Comme rtl: correspond à tout élément qui a un ancêtre dir="rtl", il s’applique encore dans un élément dir="ltr" placé sur une page arabe. Sur cet élément, ltr: (sa propre direction) et rtl: (son ancêtre) correspondent tous les deux, et la règle rtl: gagne parce qu’elle vient après. C’est parfois exactement ce qu’on veut : le champ de téléphone de l’extrait plus haut est en dir="ltr", donc son ps-11 ajoute l’espace à gauche, alors que l’icône est à droite de la page arabe, et rtl:pe-11 ajoute aussi l’espace à droite. Quand vous avez besoin de la direction propre de l’élément, utilisez une variante arbitraire basée sur la pseudo-classe :dir(), prise en charge par tous les navigateurs actuels :
// Matches when this element itself is right to left,
// not when any ancestor has dir="rtl"
<IconChevronRight className="[&:dir(rtl)]:-scale-x-100" />
4. Ce qui ne doit pas s’inverser
L’algorithme bidirectionnel du navigateur affiche déjà de gauche à droite les mots latins et les chiffres au milieu d’un texte arabe. Les problèmes viennent des séquences qui mélangent chiffres et ponctuation : un numéro de téléphone comme +1 555 010 2030, un prix avec un code de devise, une plage de valeurs ou un compteur comme 3 / 12. Leurs espaces et symboles prennent la direction de l’arabe qui les entoure, et des morceaux de la séquence peuvent sortir dans le mauvais ordre. La solution est d’isoler la séquence dans son propre îlot de gauche à droite.
Nombres et chiffres latins
Beaucoup d’interfaces arabes, dont les templates Aniq UI cités ici, utilisent les chiffres latins (0 à 9) en arabe. Que Intl les produise dépend de l’étiquette de langue et des données ICU du navigateur, alors demandez-les explicitement avec l’extension -u-nu-latn. Voici le formateur du dashboard restaurant :
// 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);
Sous Node.js 24 (ICU 78), la région décide des chiffres, et l’extension l’emporte :
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"
Utilisez la même étiquette pour les dates, les heures et les devises, et sachez que Intl ajoute des marques invisibles de droite à gauche (U+200F) autour des devises et des dates en arabe. Tenez-en compte quand vous comparez des chaînes formatées dans vos tests.
Prix
Le template immobilier enveloppe chaque prix dans <bdi dir="ltr">, ce qui l’isole de l’arabe qui l’entoure et fixe son ordre. tabular-nums donne la même largeur à tous les chiffres pour que les montants s’alignent dans les listes :
// 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>
Téléphones, e-mails et URL
Les numéros de téléphone, les adresses e-mail, les URL, les codes de langue et les compteurs s’écrivent toujours de gauche à droite. Le même template les marque un par un, et donne au champ de téléphone rtl:text-end : le numéro se tape de gauche à droite mais reste aligné avec les libellés arabes au-dessus :
// 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>
Code et logos
Le code s’écrit toujours de gauche à droite. Dans un contenu arabe, rendez <pre> et <code> en ligne avec dir="ltr" (ou direction: ltr dans la feuille de style qui affiche votre contenu). Un logotype latin, c’est pareil : le nom de marque du template immobilier est un span dir="ltr", donc ses lettres et son espacement ne bougent jamais. Les photos et illustrations ne sont jamais inversées non plus, car une photo retournée montre du texte en miroir et un produit à l’envers.
// 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>
Graphiques et axes temporels
Les graphiques sont une décision de design. Certains produits arabes gardent le temps de gauche à droite, comme les nombres sur l’axe ; d’autres le font aller de droite à gauche pour suivre le sens de lecture. Les deux se défendent ; l’important est que la bibliothèque de graphiques garde un système de coordonnées stable de gauche à droite. Le dashboard restaurant d’Aniq UI place le graphique dans un conteneur dir="ltr", puis inverse volontairement l’axe du temps en arabe et déplace l’axe des valeurs à droite :
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>

Îlots dir="ltr", <bdi>, dir="auto" et unicode-bidi
dir="ltr"sur un élément crée un îlot de gauche à droite. Les navigateurs isolent déjà du texte voisin un élément qui porte l’attributdir(la feuille de style par défaut de HTML lui donneunicode-bidi: isolate), donc il faut rarement plus.<bdi>isole une séquence en ligne dont vous ne connaissez pas forcément la direction, comme un nom d’utilisateur dans une phrase. Ajoutezdir="ltr"quand vous la connaissez, comme pour les prix.dir="auto"laisse le navigateur choisir la direction d’après le premier caractère fort. Utilisez-le pour le texte écrit par les utilisateurs, qui peut être dans l’une ou l’autre langue :
// Text written by users can be in either language: let the browser decide
<p dir="auto">{review.body}</p>
Tailwind CSS n’a pas d’utilitaire unicode-bidi, et la classe isolate est tout autre chose (isolation: isolate, qui crée un contexte d’empilement). Vous pourriez écrire une propriété arbitraire comme [unicode-bidi:isolate], mais MDN déconseille aux auteurs de modifier unicode-bidi, et l’attribut dir ou <bdi> fait le même travail dans le balisage.
5. Polices arabes, interligne et espacement des lettres
Charger une police arabe
Beaucoup de polices d’interface latines n’ont pas de glyphes arabes : le navigateur se rabat sur une police système et les pages arabes ressemblent à un autre produit. Chargez une police par écriture. Le template restaurant associe Inter et IBM Plex Sans Arabic via 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}`}>
Il change ensuite de famille sous [dir="rtl"]. La police arabe vient en premier et Inter en second, donc les mots latins dans un texte arabe (prix, noms de marque, e-mails) retombent sur Inter et ont le même aspect que sur les pages anglaises :
/* 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;
}
Le template immobilier fait de même avec des fichiers Noto Sans Arabic et Manrope hébergés sur le site, chacun déclaré avec un unicode-range, si bien qu’une page ne télécharge une police que si elle contient du texte dans cette écriture. Un sélecteur sur [dir="rtl"] évite d’ajouter une classe à chaque composant.
Interligne
Les lettres arabes ont de hautes hampes et de profonds jambages, et les signes diacritiques se placent au-dessus et en dessous. Un interligne réglé pour un titre latin (1.1 est courant) peut faire se toucher ou rogner les lignes arabes. Le dashboard restaurant aère ses titres de page en arabe uniquement :
// 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"
Espacement des lettres
L’arabe est une écriture liée, et la note de MDN sur letter-spacing le dit clairement : un espacement appliqué à l’arabe peut donner l’impression d’un texte cassé. Une approche resserrée sur les titres latins et un espacement large sur les petits libellés en majuscules sont courants, alors remettez-les à zéro avec rtl:tracking-normal, comme dans la ligne du libellé ci-dessus (tirée du template immobilier), ou avec letter-spacing: 0 dans la règle [dir="rtl"]. L’arabe n’a pas de majuscules non plus, donc uppercase n’y fait rien : un libellé qui compte sur les capitales et l’espacement en anglais a besoin de graisse ou de couleur pour ressortir en arabe.
6. Checklist de test RTL
Passez-la sur chaque page en arabe, en thème clair et sombre, en largeur mobile et bureau :
- L’attribut est dans le HTML du serveur. Affichez le code source de la page (pas l’inspecteur) et cherchez
<html lang="ar" dir="rtl">. Rechargez avec un réseau lent et guettez une inversion après le chargement. - La mise en page est en miroir. La barre latérale, le logo, la navigation principale et l’action principale sont à droite. Rien n’est inversé deux fois.
- Panneaux, menus et notifications s’ouvrent du bon côté et s’affichent de droite à gauche, y compris ceux rendus dans des portails.
- Flèches et chevrons pointent dans le bon sens dans les fils d’Ariane, la pagination, les carrousels, les boutons « suivant » et les liens « voir tout ». Les boutons de lecture et les horloges ne s’inversent pas.
- Champs. Icônes, boutons d’effacement et icônes de validation sont à droite. Les champs de téléphone, d’e-mail, d’URL et de code se tapent de gauche à droite.
- Nombres. Les chiffres sont ceux que vous avez choisis, prix et numéros de téléphone se lisent dans le bon ordre, et les compteurs comme 3 / 12 ne sont pas mélangés.
- Texte mixte. Un nom de marque anglais dans une phrase arabe garde la ponctuation à sa place.
- Typographie. La police arabe se charge (DevTools affiche la police rendue), les titres ne sont pas rognés et aucun texte arabe n’a d’espacement des lettres.
- Clavier. Les flèches dans les onglets, curseurs et carrousels suivent le sens de lecture de l’écran.
- Graphiques : la convention choisie est la même sur tous les graphiques.
- Classes physiques. Cherchez les utilitaires left et right dans le code et tranchez pour chaque résultat :
# 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
Sur les applications Aniq UI où nous l’avons lancée, la commande renvoie entre 2 et 10 lignes par application : éléments centrés, paires symétriques left-0 right-0, un curseur avant/après physique et des lignes qui ont déjà une surcharge rtl:. C’est la taille de liste qu’il vous faut : assez courte pour lire chaque ligne.
FAQ
Tailwind CSS prend-il en charge le RTL ?
Oui. Tailwind CSS propose des utilitaires logiques comme ms-*, pe-*, inset-s-*, text-start, border-s et rounded-e-*, qui suivent l’attribut dir, ainsi que les variantes rtl: et ltr: pour les styles qui doivent changer selon la direction. Les deux sont intégrés.
Faut-il un plugin RTL avec Tailwind CSS 4 ?
Non. Les utilitaires logiques font partie de Tailwind CSS depuis la version 3.3, et les variantes rtl: et ltr: sont dans le cœur. Définissez dir sur <html> et utilisez-les.
Faut-il utiliser start-* ou inset-s-* ?
Tailwind CSS 4.2 a déprécié start-* et end-* au profit de inset-s-* et inset-e-*. Les deux compilent vers inset-inline-start et inset-inline-end, donc le code existant continue de fonctionner. Utilisez inset-s-* dans le nouveau code si vous êtes en 4.2 ou plus.
Utilitaires logiques ou variante rtl: ?
Les utilitaires logiques d’abord. ms-3 remplace ltr:ml-3 rtl:mr-3 par une seule classe qui ne peut pas se désynchroniser. Gardez rtl: pour ce que les propriétés logiques ne savent pas exprimer : icônes en miroir, translate-x, sens des dégradés, origines de transformation et réglages typographiques comme l’interligne et l’espacement des lettres.
Comment retourner une icône en RTL avec Tailwind ?
Ajoutez rtl:-scale-x-100 à l’icône, ce qui la met en miroir horizontal. rtl:rotate-180 fonctionne aussi pour les flèches et chevrons symétriques. Ne retournez que les icônes qui indiquent une direction ; les commandes de média, les horloges et les logos restent tels quels.
Comment garder les chiffres latins en arabe ?
Formatez les nombres avec une étiquette de langue qui contient -u-nu-latn, par exemple new Intl.NumberFormat("ar-u-nu-latn") ou "ar-SA-u-nu-latn". Utilisez la même étiquette pour les dates et les devises.
dir="rtl" sur <body> ou sur un div suffit-il ?
Placez-le sur <html>. Le contenu rendu dans des portails, comme les boîtes de dialogue et les notifications, se trouve hors de votre conteneur et n’hérite que d’une direction définie sur <html> ou <body>, et un attribut à la racine donne aussi aux lecteurs d’écran et au sélecteur :dir() une seule source de vérité.
Templates avec le RTL intégré
Chaque principe de ce guide vient de code livré dans les templates Aniq UI. Tous sont en Next.js avec Tailwind CSS 4 et next-intl, en anglais et en arabe avec une vraie mise en page de droite à gauche, et des démos en ligne que vous pouvez passer en arabe. Si vous préférez partir d’une base où ce travail est fait, les neuf templates de dashboard arabe RTL sont comparés côte à côte, et les autres (restaurant, immobilier, LMS, e-commerce et landing pages) sont sur la page des templates.
Ce que coûte chacun, en un seul paiement, avec le code source et des démos en ligne que vous pouvez passer en arabe :
- Sites et boutiques (restaurant, immobilier, LMS, e-commerce, location de vacances, billetterie) : à partir de $39.
- Landing pages (SaaS et entreprise) : $39.
- Dashboards d’administration : à partir de $49. Les 9 comparés.
- Full stack (site, dashboard et API ensemble) : $149.
- Les 10 templates : le pack, pour une fraction des prix séparés.
Parcourez les templates et ouvrez n’importe quelle démo en arabe avant d’acheter.
Guides associés
- Templates de dashboard arabe RTL pour Next.js : 9 dashboards Aniq UI
- Meilleurs templates de dashboard RTL avec support de l’arabe (2026)
- Comment lancer un site SaaS en anglais et en arabe avec Next.js
- next-intl et Next.js 16 : configuration de proxy.ts, root params et cache
- Ressources pour développeurs de templates Next.js
Sources
- Tailwind CSS : prise en charge du RTL (variantes rtl et ltr)
- Tailwind CSS : marges et propriétés logiques
- Tailwind CSS : top, right, bottom, left (inset-s et inset-e)
- Tailwind CSS : épaisseur de bordure
- Tailwind CSS : arrondis
- Tailwind CSS : image de fond (dégradés linéaires)
- Journal des versions de Tailwind CSS (4.2.0 : inset-s, inset-e, start et end dépréciés)
- Tailwind CSS v3.3 : RTL simplifié avec les propriétés logiques
- next-intl : configuration de l’App Router avec routage i18n
- MDN : la pseudo-classe :dir()
- MDN : l’élément bdi
- MDN : unicode-bidi
- MDN : letter-spacing (internationalisation)
- MDN : Intl.NumberFormat
- Standard HTML : rendu du texte bidirectionnel
- Material Design : bidirectionnalité
Vérifié en novembre 2026.
Vous pourriez aussi aimer

Erreur proxy.ts Next.js 16 : missing expected function export
Corrigez l'erreur Next.js 16 « Proxy is missing expected function export name » : causes reproduites sur 16.4.5, exports qui compilent, next-intl.

Templates de dashboard arabe RTL pour Next.js : 9 dashboards Aniq UI
Neuf templates de dashboard Next.js signés Aniq UI, en anglais et en arabe, avec une vraie mise en page RTL, thèmes clair et sombre et démos en arabe.

Meilleurs templates de dashboard RTL avec support de l’arabe (2026)
Comparez 10 templates de dashboard RTL avec support de l’arabe : Next.js, MUI et Tailwind, gratuits et premium, plus un test RTL de cinq minutes.