Building an Arabic website that feels truly professional is harder than it looks. It is not just about setting dir="rtl" on the HTML element and calling it done. Arabic typography has its own rules, bidirectional text is a genuine engineering challenge, SEO for Arabic-language audiences requires specific considerations, and performance matters even more when your users are on mobile networks in Cairo, Riyadh, or Dubai. I have built several Arabic-first websites with Next.js and each one taught me something new. This guide covers everything I know.
Why Next.js for Arabic Sites?
Next.js is the right choice for Arabic websites for several reasons beyond the usual React benefits. Its built-in i18n routing handles /ar and /en route prefixes automatically. Server-side rendering ensures Arabic content is in the initial HTML — critical for SEO since Arabic search engines (and Google's Arabic crawlers) need to read your content without executing JavaScript. The App Router's layout system makes it clean to wrap different language routes in different <html lang> and dir attributes. And next/font handles Arabic font loading efficiently without layout shifts.
RTL Setup: The Right Way
The wrong way is to add a global dir="rtl" and then override everything with margin and padding hacks. The right way is to use CSS logical properties from the start, and to control dir at the <html> level per locale:
// app/[locale]/layout.tsx
import { notFound } from 'next/navigation';
const locales = ['en', 'ar'] as const;
type Locale = typeof locales[number];
export default function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: Locale };
}) {
if (!locales.includes(locale)) notFound();
const isRTL = locale === 'ar';
return (
{children}
);
}
i18n Configuration
Next.js 15's App Router handles i18n without a plugin. Create a next.config.ts with your locales, then use the [locale] dynamic segment in your app directory. For translation strings, I prefer a simple JSON file approach with a typed useTranslation hook over bringing in a full library like next-intl for smaller sites:
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
// App Router handles i18n via the [locale] segment
// No next.config i18n block needed with App Router
};
export default config;
// middleware.ts — redirect / to /en or /ar based on Accept-Language
import { NextRequest, NextResponse } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/;
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (PUBLIC_FILE.test(pathname) || pathname.includes('/_next')) return;
const hasLocale = ['en', 'ar'].some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return;
const acceptLanguage = request.headers.get('accept-language') ?? '';
const preferArabic = acceptLanguage.startsWith('ar');
return NextResponse.redirect(
new URL(`/${preferArabic ? 'ar' : 'en'}${pathname}`, request.url)
);
}
// lib/translations.ts
import en from '@/messages/en.json';
import ar from '@/messages/ar.json';
const messages = { en, ar } as const;
type Locale = keyof typeof messages;
export function getTranslations(locale: Locale) {
return messages[locale];
}
// Usage in a Server Component:
// const t = getTranslations(locale);
// <h1>{t.hero.title}</h1>
CSS Logical Properties — The Foundation
This is the single most important technical decision for an RTL site. Physical CSS properties like margin-left, padding-right, border-left, and text-align: left are direction-specific and break in RTL. Logical properties map to "start" and "end" relative to the current text direction:
/* WRONG — breaks in RTL */
.card {
margin-left: 1rem;
padding-right: 1.5rem;
border-left: 3px solid var(--accent);
text-align: left;
}
/* RIGHT — works in both LTR and RTL */
.card {
margin-inline-start: 1rem;
padding-inline-end: 1.5rem;
border-inline-start: 3px solid var(--accent);
text-align: start;
}
/* Tailwind CSS v3.3+ has logical property utilities */
/* ms-4 = margin-inline-start: 1rem */
/* pe-6 = padding-inline-end: 1.5rem */
/* border-s-2 = border-inline-start-width: 2px */
/* text-start = text-align: start */
When using Tailwind, adopt the logical property utilities (ms-, me-, ps-, pe-, border-s-, border-e-, start-, end-) everywhere instead of l- and r-. This is a convention your whole team needs to agree on at the start of the project — retrofitting it later is painful.
Arabic Typography
Choosing the right Arabic font is not a minor decision. System Arabic fonts (like the default on iOS and Android) are functional but not beautiful at display sizes. For professional sites, load a custom Arabic font. My recommendations based on use case:
- Tajawal — Clean, modern, excellent for body text and UI elements. Used on this portfolio.
- Cairo — Slightly more personality, great for headings and marketing copy.
- IBM Plex Sans Arabic — Professional and technical feel, excellent for developer tools and SaaS dashboards.
- Noto Naskh Arabic — Traditional and high-readability for long-form content.
// app/[locale]/layout.tsx
import { Tajawal, Cairo } from 'next/font/google';
const tajawal = Tajawal({
subsets: ['arabic'],
weight: ['300', '400', '500', '700'],
variable: '--font-arabic',
display: 'swap', // Important: prevents invisible text during font load
});
const cairo = Cairo({
subsets: ['arabic', 'latin'],
weight: ['400', '600', '700'],
variable: '--font-heading-ar',
display: 'swap',
});
export default function Layout({ children, params: { locale } }) {
const isRTL = locale === 'ar';
return (
<html
lang={locale}
dir={isRTL ? 'rtl' : 'ltr'}
className={`${tajawal.variable} ${cairo.variable}`}
>
<body className={isRTL ? 'font-arabic' : 'font-sans'}>
{children}
</body>
</html>
);
}
/* globals.css */
:root {
--font-sans: 'Outfit', system-ui, sans-serif;
--font-arabic: var(--font-arabic), 'Tajawal', 'Arial Unicode MS', sans-serif;
}
[dir="rtl"] {
font-family: var(--font-arabic);
font-size: 1.05em; /* Arabic text often benefits from slightly larger size */
line-height: 1.8; /* Arabic glyphs are taller, need more line spacing */
letter-spacing: 0; /* Never apply positive letter-spacing to Arabic */
}
Handling Bidirectional Text
The trickiest scenario is mixed-language content: an Arabic product description with an English brand name, or an Arabic article with English code snippets. The Unicode Bidirectional Algorithm handles most of this automatically, but numbers, punctuation, and inline formatting can still cause visual glitches.
// components/BidiText.tsx
// Wraps text that might contain mixed-direction content
export function BidiText({ children }: { children: React.ReactNode }) {
return (
<span
style={{ unicodeBidi: 'plaintext' }}
dir="auto"
>
{children}
</span>
);
}
// For numbers in an Arabic context — always use LTR
export function NumericValue({ value }: { value: string | number }) {
return (
<span dir="ltr" style={{ display: 'inline-block' }}>
{value}
</span>
);
}
// Usage:
// <p>المنتج: <BidiText>iPhone 16 Pro Max</BidiText></p>
// <p>السعر: <NumericValue value="SAR 5,299" /></p>
Arabic Form Validation
Arabic form inputs need special attention. Phone number formats differ by country (Egypt: 01X-XXXX-XXXX, Saudi: 05X-XXXX-XXXX). Arabic names can include titles and particles that confuse simple regex validators. And the direction of input fields needs to switch based on what the user is typing:
// lib/validations-ar.ts
import { z } from 'zod';
export const arabicNameSchema = z
.string()
.min(2, 'الاسم يجب أن يكون حرفين على الأقل')
.max(60, 'الاسم طويل جدًا')
.regex(/^[\u0600-\u06FF\s\u064B-\u065F']+$/, 'الاسم يجب أن يحتوي على أحرف عربية فقط');
export const saudiPhoneSchema = z
.string()
.regex(/^(05|5)(5|0|3|6|4|9|1|8|7)([0-9]{7})$/, 'رقم الهاتف السعودي غير صحيح');
export const egyptianPhoneSchema = z
.string()
.regex(/^(01)[0-9]{9}$/, 'رقم الهاتف المصري غير صحيح');
// Auto-detect text direction for inputs
export function useInputDirection() {
const detectDir = (value: string): 'rtl' | 'ltr' | 'auto' => {
if (!value) return 'auto';
const firstChar = value.trim()[0];
const arabicRange = /[\u0600-\u06FF]/;
return arabicRange.test(firstChar) ? 'rtl' : 'ltr';
};
return detectDir;
}
SEO for Arabic Websites
Arabic SEO has specific requirements. The most critical: use hreflang alternate links for every page, and ensure your Arabic content is served as real text (not images of text). Arabic Google searches have different keyword patterns — Arabic users often use Modern Standard Arabic (MSA) for searches even if they speak dialect. Use Google Keyword Planner with Arabic language selected to research properly.
// app/[locale]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({
params: { locale },
}: {
params: { locale: string };
}): Promise {
const isAr = locale === 'ar';
return {
title: isAr
? 'متجر الإلكترونيات — أفضل الأسعار في السعودية'
: 'Electronics Store — Best Prices in Saudi Arabia',
description: isAr
? 'اكتشف أحدث الأجهزة الإلكترونية بأسعار تنافسية مع توصيل سريع لجميع مناطق المملكة'
: 'Discover the latest electronics at competitive prices with fast delivery across Saudi Arabia',
alternates: {
canonical: `https://example.com/${locale}`,
languages: {
'ar-SA': 'https://example.com/ar',
'en-US': 'https://example.com/en',
},
},
openGraph: {
locale: isAr ? 'ar_SA' : 'en_US',
alternateLocale: isAr ? 'en_US' : 'ar_SA',
},
};
}
Performance for MENA Audiences
Mobile internet in Saudi Arabia and Egypt is fast (4G/5G coverage is high), but users are sensitive to Core Web Vitals because they compare experiences across global apps. Arabic fonts are typically larger than Latin fonts because the character set is richer. Two specific optimizations matter here:
First, subset your Arabic fonts to include only the Unicode blocks you need. The full Arabic Unicode range (U+0600–U+06FF) covers Modern Arabic, plus extended blocks for Persian and Urdu. If you only support Arabic, you can subset to U+0600–U+06FF which saves 30–50KB. The next/font Google Fonts integration handles this automatically with subsets: ['arabic'].
Second, preload your Arabic font for Arabic locale pages. A Largest Contentful Paint caused by a late-loading Arabic font is a common issue:
// app/ar/layout.tsx — preload Arabic font
export default function ArabicLayout({ children }) {
return (
<>
<link
rel="preload"
href="/fonts/tajawal-v28-arabic-400.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{children}
</>
);
}
Common Mistakes to Avoid
After building multiple Arabic sites, here are the mistakes I see repeatedly:
- Applying
letter-spacingto Arabic text. Arabic script is cursive — letters connect. Letter spacing breaks the connections and makes text look broken. Always setletter-spacing: 0for Arabic. - Using CSS transforms to flip icons. Directional icons (arrows, chevrons) should flip in RTL. Use CSS logical transforms:
transform: scaleX(-1)conditionally, or better, use directionally appropriate SVG icons. - Forgetting to flip flex/grid ordering. Flexbox and grid respect the
dirattribute, but only if you useflex-start/flex-endnotleft/right. - Using absolute positioning with left/right. Switch to
inset-inline-startandinset-inline-end. - Not testing on actual Arabic content. English placeholder text does not reveal RTL layout bugs. Use real Arabic content in your development from day one.
Hosting for MENA
For Arabic websites targeting Saudi Arabia and Egypt, edge network proximity matters. Vercel's edge network has nodes in the region and is my default recommendation for Next.js deployments. Cloudflare Pages is a strong alternative. If you need more control, AWS has data centers in Bahrain (me-south-1) and UAE (me-central-1), which gives you sub-50ms latency to most Gulf users. For Egypt-heavy traffic, Cloudflare's network typically performs better than AWS since they have multiple PoPs across North Africa.