チュートリアル//10 読了時間

右から左(RTL)サポート:CSSとReactの実践的な実装ガイド

Eray Gündoğmuş
共有

右から左(RTL)サポート:CSSとReactの実践的な実装ガイド

RTLサポートは、どうしても避けられなくなるまで後回しにされがちな機能の一つです。そして実際にその時が来ると、チームは製品全体がテキストが左から右に流れることを前提として構築されていることに気づき、RTL対応への改修はすべてのレイアウトファイルを触ることを意味します。

このガイドは、最初から正しくやること——あるいは少なくとも、できるだけクリーンに改修すること——を目指しています。CSSの論理プロパティ、dir属性、フレックスボックスの挙動、Reactのパターン、Tailwindのユーティリティ、その他すべてを網羅します。

RTLが必要な言語は?

実装の前に、対象ユーザーを理解しましょう。RTL表記体系には以下が含まれます:

  • アラビア語 — 4億人以上のネイティブスピーカー、26カ国の公用語
  • ヘブライ語 — 1000万人以上のスピーカー、高GDP市場で主流
  • ペルシャ語/ファルシー語 — 8000万人以上のスピーカー(イラン、アフガニスタン、タジキスタン)
  • ウルドゥー語 — 7000万人以上のネイティブスピーカー、パキスタンの共用語
  • パシュトゥー語 — 6000万人以上のスピーカー
  • シンディー語、ウイグル語、クルド語(ソラニー) — より小規模だが重要なユーザー層

アラビア語とヘブライ語の市場だけでも、莫大なEコマースおよびSaaSの収益ポテンシャルがあります。RTLサポートのない製品はこれらの地域ではリリースできません——レイアウトをローカライズせずにコンテンツをローカライズしても意味がありません。

CSSの論理プロパティ:基盤

RTLサポートのためにCSSに対して行える最大の改善は、物理プロパティから論理プロパティへの切り替えです。物理プロパティ(margin-leftpadding-rightborder-left)は画面の端に固定されています。論理プロパティ(margin-inline-startpadding-inline-endborder-inline-start)はドキュメントの書字方向に基づいて自動的に適応します。

この一つの変更だけで、RTLレイアウトの問題の約80%が解決されます。

対応表:

物理プロパティ論理プロパティの等価物
margin-leftmargin-inline-start
margin-rightmargin-inline-end
padding-leftpadding-inline-start
padding-rightpadding-inline-end
border-leftborder-inline-start
border-rightborder-inline-end
leftinset-inline-start
rightinset-inline-end
margin-topmargin-block-start
margin-bottommargin-block-end
padding-toppadding-block-start
padding-bottompadding-block-end
text-align: lefttext-align: start
text-align: righttext-align: end
widthinline-size
heightblock-size

2026年のブラウザサポート: 優秀です。すべての主要ブラウザが何年もCSSの論理プロパティをサポートしています。ためらわずに使用できます。

/* 変更前 — RTLで壊れる */
.card {
  margin-left: 1rem;
  padding-right: 1.5rem;
  border-left: 2px solid var(--accent);
  text-align: left;
}

/* 変更後 — LTRとRTLの両方で動作する */
.card {
  margin-inline-start: 1rem;
  padding-inline-end: 1.5rem;
  border-inline-start: 2px solid var(--accent);
  text-align: start;
}

LTRモードでは、margin-inline-startmargin-leftに解決されます。RTLではmargin-rightに解決されます。CSSを一度書くだけで、両方向が正しく動作します。

dir属性

HTML要素レベルで方向を設定します:

<html lang="ar" dir="rtl">

または、混在コンテンツがある場合は要素ごとに:

<p dir="rtl">مرحباً بالعالم</p>
<p dir="ltr">Hello world</p>

CSSのdirectionプロパティも同じことをCSSで行います:

.arabic-content {
  direction: rtl;
}

論理プロパティでは対応できない場合のRTL固有のオーバーライドに[dir="rtl"]セレクターパターンが役立ちます:

/* デフォルト (LTR) */
.nav-icon {
  transform: none;
}

/* 方向性アイコンのRTLオーバーライド */
[dir="rtl"] .nav-icon--arrow {
  transform: scaleX(-1);
}

**unicode-bidi: bidi-override**はコンテンツに関係なく特定の方向を強制します。これは控えめに使用してください——通常はコードブロックや並べ替えてはならないコンテンツにのみ必要です:

.code-block {
  direction: ltr;
  unicode-bidi: isolate;
}

RTLにおけるフレックスボックスとグリッド

本当に良いニュースがあります:フレックスボックスとCSSグリッドはドキュメントの方向を自動的に尊重します。

.nav {
  display: flex;
  flex-direction: row;
  gap: 1rem;
}

LTRでは、アイテムは左から右に流れます。RTLでは、同じCSSによってアイテムが右から左に流れます。何も変更する必要はありません。

自動的に機能するもの:

  • flex-direction: rowはRTLで反転する
  • justify-content: flex-startはインラインの開始位置に揃える(RTLでは右)
  • グリッド列の順序が方向を尊重する
  • グリッドテンプレートエリアが正しく適応する

手動で対応が必要なもの:

/* このtransformは自動的にフリップしない */
.slide-in {
  transform: translateX(-100%);
}

/* 論理プロパティの等価物で修正 */
[dir="rtl"] .slide-in {
  transform: translateX(100%);
}

left/rightを使った位置指定もフリップしません:

/* RTLで壊れる */
.tooltip {
  position: absolute;
  left: 100%;
}

/* 代わりに論理プロパティを使用 */
.tooltip {
  position: absolute;
  inset-inline-start: 100%;
}

RTL修正が必要な一般的なパターン

ナビゲーション矢印とシェブロン

方向を示す矢印アイコンはRTLでフリップする必要があります:

/* 方向性アイコンのみフリップ */
[dir="rtl"] .icon--arrow-right,
[dir="rtl"] .icon--chevron-right,
[dir="rtl"] .icon--next,
[dir="rtl"] .icon--forward {
  transform: scaleX(-1);
}

フリップしてはいけないもの: チェックマーク、警告三角形、ロゴ、ユーザーアバター、アップロード/ダウンロードアイコン、メディアコントロール(再生/一時停止)。これらは方向性を持っていません。

パンくずリスト

パンくずアイテム間のセパレーターは反転する必要があります:

.breadcrumb-separator::before {
  content: "/";
}

[dir="rtl"] .breadcrumb-separator::before {
  content: "\\";
  /* または中立のセパレーター文字(•など)を使用 */
}

より良い方法:中立のセパレーター文字や、フリップできるSVGを使用してください。

プログレスバー

塗りつぶしの方向は視覚的に重要です:

.progress-fill {
  width: var(--progress);
  /* LTRでは左から塗りつぶす */
  /* RTLでは右から塗りつぶす必要がある */
  transform-origin: inline-start;
}

[dir="rtl"] .progress-fill {
  margin-inline-start: auto;
  /* または: ミラーリングされたグラデーションを使用 */
}

ボックスシャドウ

深みや高さを示すシャドウは方向性を持って見えることが多いです:

/* LTR — 右側のシャドウ */
.card {
  box-shadow: 4px 0 8px rgba(0,0,0,0.1);
}

/* RTL — シャドウは左側にあるべき */
[dir="rtl"] .card {
  box-shadow: -4px 0 8px rgba(0,0,0,0.1);
}

Reactの実装

方向の状態をコンテキストとして構造化し、任意のコンポーネントがそれを消費できるようにします:

// 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);
}

アプリのルートで、ロケール設定から方向を渡します:

// 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>
  );
}

方向を知る必要があるコンポーネントはフックを消費します:

function NavArrow() {
  const direction = useDirection();

  return (
    <svg
      style={{
        transform: direction === "rtl" ? "scaleX(-1)" : "none"
      }}
      aria-hidden="true"
    >
      {/* arrow path */}
    </svg>
  );
}

CSSモジュールを使用する場合: データ属性を追加してそれをセレクターとして使用します:

<div data-direction={direction} className={styles.container}>
/* component.module.css */
.container {
  padding-inline-start: 1rem;
}

/* 論理プロパティで対応できないものだけオーバーライド */
.container[data-direction="rtl"] .icon {
  transform: scaleX(-1);
}

Better i18nのようなプラットフォームを使用している場合、ロケールの方向はSDKからロケール自体と一緒に提供されるため、追加のセットアップなしにロケールを導出する同じ箇所でdirを導出できます。

Tailwind CSS RTLサポート

Tailwindにはrtl:ltr:バリアントが標準で含まれています。設定で有効にします:

// tailwind.config.js
module.exports = {
  // ...
  future: {
    hoverOnlyWhenSupported: true,
  },
};

バリアントは祖先要素にdir="rtl"またはdir="ltr"がある場合に有効になります。

基本的な使用法:

<!-- マージンは方向に基づいてフリップ -->
<div class="ms-4 me-2">
  <!-- ms- = margin-inline-start, me- = margin-inline-end -->
  <!-- これらはTailwindの論理プロパティユーティリティ -->
</div>

<!-- 明示的な方向オーバーライド -->
<button class="ltr:ml-4 rtl:mr-4">送信</button>

<!-- アイコンのフリップ -->
<svg class="rtl:scale-x-[-1]">...</svg>

Tailwindの論理プロパティユーティリティ(ms-me-ps-pe-border-sborder-estart-end-)が正しいデフォルトの選択肢です——物理ユーティリティの代わりに使用し、バリアントなしでRTLを処理します:

<!-- 変更前: 物理ユーティリティ、RTLで壊れる -->
<nav class="pl-6 border-l-2 text-left">

<!-- 変更後: 論理ユーティリティ、両方向で動作 -->
<nav class="ps-6 border-s-2 text-start">

rtl:ltr:バリアントは、論理プロパティでは対応できないもの——アイコンのtransformやアニメーションのoriginなど——にのみ使用してください。

RTLレイアウトのテスト

Chrome DevTools

RTLをプレビューする最も手早い方法:DevToolsを開き、Renderingタブに移動して——ただし、より便利なのはElementsパネルでルート要素にdir="rtl"を追加する方法です。ページのリロードは不要です。

Playwright

両方のモードでレイアウトを検証する方向認識テストを作成します:

// rtl.spec.ts
import { test, expect } from "@playwright/test";

test.describe("RTLレイアウト", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/");
    await page.evaluate(() => {
      document.documentElement.setAttribute("dir", "rtl");
    });
  });

  test("RTLでナビゲーションが正しくレンダリングされる", async ({ page }) => {
    const nav = page.locator("nav");
    await expect(nav).toBeVisible();

    // RTLでメニューが右側にあることを確認
    const navBox = await nav.boundingBox();
    const viewportWidth = page.viewportSize()?.width ?? 1280;
    expect(navBox!.x).toBeGreaterThan(viewportWidth / 2);
  });

  test("フォームの入力が正しく揃う", async ({ page }) => {
    const label = page.locator('label[for="email"]');
    const input = page.locator("#email");

    const labelBox = await label.boundingBox();
    const inputBox = await input.boundingBox();

    // RTLでは、ラベルは入力の右側または上部にあるべき
    // これはレイアウトによって異なるため、アサーションを適宜調整する
    expect(labelBox).toBeTruthy();
    expect(inputBox).toBeTruthy();
  });
});

ビジュアルリグレッション

dir="rtl"バリアントでビジュアルリグレッションテストを実行します。Playwrightの組み込みスクリーンショット比較を使用して:

test("RTLホームページのスナップショット", async ({ page }) => {
  await page.goto("/");
  await page.evaluate(() => {
    document.documentElement.setAttribute("dir", "rtl");
  });
  await expect(page).toHaveScreenshot("homepage-rtl.png");
});

手動QAチェックリスト

  • ナビゲーションアイテムが右から左に流れている
  • サイドバーが右側にある
  • フォームラベルが正しく揃っている
  • 方向性アイコン(矢印、シェブロン)がフリップされている
  • プログレスバーが右から塗りつぶされる
  • ドロップダウンが正しい方向に開く
  • モーダル/ドロワーが正しい側から開く
  • パンくずのセパレーターが正しい方向を向いている
  • テキストの揃いが全体を通して正しい
  • スクロールバーが左側に表示される(ブラウザが自動的に処理する)
  • RTLコンテンツ内の数字とラテン文字がLTRのまま

RTLのフォントとタイポグラフィ

アラビア語とヘブライ語には適切なフォントが必要です——デフォルトのシステムフォントでは描画品質が低い場合があります。

推奨フォント:

  • Noto Sans Arabic — Googleの包括的なカバレッジ、無料、Notoファミリーと一致
  • IBM Plex Arabic — テクニカル/開発者ツールに優れており、IBM Plex Sansと組み合わせられる
  • Cairo — モダンでクリーン、UIに適している
  • Tajawal — ジオメトリック、サンセリフのLTRフォントとよく合う
@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; /* アラビア語は少し大きめがより読みやすい */
  line-height: 1.8;  /* アラビア語はラテン語より縦のスペースが必要 */
}

:lang(he) {
  font-family: "Noto Sans Hebrew", "Segoe UI", system-ui, sans-serif;
  line-height: 1.6;
}

主要なタイポグラフィの調整:

  • アラビア語テキストは通常、同じサイズの等価なラテン語テキストと比較して1〜2px大きいフォントサイズが有効です
  • アラビア語の行の高さは1.6〜2.0が適切(ラテン語の1.4〜1.6に対して)、発音符号のため
  • アラビア語のletter-spacingは一般的に0またはわずかにマイナスにすべきで、決してプラスにしてはいけません
  • font-variant-numeric: ltrを使用すると、RTLテキスト内の数字がLTRでレンダリングされ続けます

双方向テキスト(Bidi)

RTLドキュメントには、LTRの断片が含まれることが多いです:英語のブランド名、数字、URL、コード。Unicode双方向アルゴリズムがほとんどのケースを自動的に処理しますが、複雑なレイアウトには明示的な制御が必要です。

<bdi>要素は、周囲のテキストからフラグメントの方向性を分離します:

<p dir="rtl">
  المستخدم <bdi>JohnDoe123</bdi> أرسل رسالة
</p>

<bdi>がないと、ユーザー名の文字によっては誤った方向でレンダリングされる可能性があります。

**unicode-bidi: isolate**はCSSで同じことを行います:

.username {
  unicode-bidi: isolate;
}

数字はRTLテキスト内で自動的にLTRのままです——Bidiアルゴリズムがこれを処理します。電話番号、価格、日付はすべて読む順序でレンダリングされます。通常は介入する必要はありません。

コードブロックは常にLTRでなければなりません:

pre, code {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start; /* startはLTRでは左 */
}

ラテン文字で書かれたブランド名や製品名は、RTLテキスト内で自然にLTRのままになります。レンダリングが正しくない場合は、<bdi>またはdir="ltr"を持つspanでラップします:

<span dir="ltr">Better i18n</span>

Better i18nのようなプラットフォームで翻訳を管理している場合、SDKはトランスレーターが追加したインライン方向マーカーをすでに含んだロケール文字列を提供するため、アプリケーションコードで文字列を後処理する必要はありません。

RTLとBetter i18n

LTRとRTLのロケールにまたがる翻訳を管理している場合、方向データはロケール設定の隣に置くべきです——コンポーネント全体に散らばらせるべきではありません。Better i18nは、翻訳文字列を提供するのと同じSDKを通じて、テキスト方向を含むロケールメタデータを提供します。ユーザーをアラビア語に切り替えると、方向は誰かが別途配線することを覚えておく必要のある独立したステップとしてではなく、ロケールのアクティベーションの一部として自動的にフリップされます。

RTL実装チェックリスト

RTLサポートを出荷する前に確認してください:

CSS

  • 物理的なmargin/padding/borderを論理プロパティに切り替えた
  • left/rightの位置指定をinset-inline-start/inset-inline-endに置き換えた
  • text-align: left/righttext-align: start/endに置き換えた
  • 方向性アイコンが[dir="rtl"]セレクターまたはrtl: Tailwindバリアントでフリップする
  • アニメーションとtransformが方向を考慮している

HTML

  • dir属性が<html>要素に設定されている
  • lang属性が正しく設定されている
  • 混在方向のインラインコンテンツに<bdi>が使用されている

React

  • 方向コンテキストプロバイダーがアプリをラップしている
  • ロケールから方向へのマッピングがすべてのRTLロケールを処理する
  • 方向を消費するコンポーネントがハードコードされた値ではなくフックを使用する

テスト

  • すべてのキーページのRTLスクリーンショットを確認した
  • 自動テストがRTLレイアウトをカバーしている
  • QAチェックリストを完了した

タイポグラフィ

  • アラビア語/ヘブライ語/ペルシャ語にRTL対応フォントを読み込んだ
  • RTLスクリプトに合わせて行の高さを調整した
  • 必要に応じてフォントサイズを調整した

RTLサポートは単一の機能ではありません——それはレイアウトシステム全体のプロパティです。うまくやっているチームは、方向をデザインシステムの第一級の次元として扱い、最初から論理プロパティを選択し、フレックスボックスとグリッドに重い作業をさせています。苦労しているチームは、CSSのあらゆる隅にLTRの前提を組み込んでしまい、今それらを一つずつオーバーライドする必要があるチームです。

論理プロパティから始めましょう。HTML要素に方向を設定してください。残りはフレックスボックスに自動的に処理させましょう。そして、方向認識が必要な特定のパターン——アイコン、transform、フォント——に対処してください。丁寧にやれば、RTLサポートはほとんどのチームが予想するよりもはるかに少ない作業です。

Better i18nは、モダンなフロントエンドチームのために構築された開発者ファーストのローカライゼーションプラットフォームです。型安全なSDK、Gitベースのワークフロー、CDNデリバリー、そしてグロッサリー適用を伴うAI翻訳——リポジトリにロケールファイルなし。

Comments

Loading comments...