Auto-detects Persian and Arabic text on web pages, then instantly applies the right direction (RTL) and a comfortable font - before you even see the page.
Dynamic RTL watches every page you open and, the moment it spots Persian or Arabic text - in a paragraph, a chat bubble, a tweet, or even an input field - it switches that element to right-to-left direction and renders it with the Vazirmatn variable font.
It is shipped as three separate builds that share the same detection logic and default font, but each follows the design language of its host application:
| Build | Target | Manifest |
|---|---|---|
chrome/ |
Chrome, Edge, Brave, Arc, Opera (Chromium) | MV3 |
firefox/ |
Firefox 121 and newer | MV3 |
safari/ |
Safari 26 and newer (temporary load) | MV3 |
Repository: https://github.com/soroush5/Dynamic-RTL
- What it does
- Highlights
- Install
- Using the browser extensions
- Custom variable font
- Sites we keep an eye on
- Performance
- Privacy
- Troubleshooting
- Repository layout
- Building from source
- Credits
- License
When a Dynamic RTL build is active:
- Pages: at
document_start(before the body is parsed) it injects a stylesheet that registers the bundled Vazirmatn variable font and a CSS class called.dynrtl-rtl. As the DOM is parsed and as new content streams in (chat messages, tweets, search results), aTreeWalker+MutationObserverfinds Persian / Arabic text and tags the nearest block ancestor. - Inputs / contenteditable: a capture-phase listener flips
dir="auto"andtext-alignon<input>,<textarea>and[contenteditable]as you type, even on a single Persian character. The CSS class never setsdirection: rtl !importanton editors, which would break editor frameworks like Slate / Lexical / ProseMirror used by Claude, Notion, X, Gemini, etc.
It supports Persian (fa), Arabic (ar) and any script in the Arabic Unicode blocks (U+0600-U+06FF, U+0750-U+077F, U+08A0-U+08FF, U+FB50-U+FDFF, U+FE70-U+FEFF).
-
No flash: in browsers, font and base styles are registered before first paint via a
document_startcontent script. -
Native look per host: each build follows the design language of its host:
- Chrome popup / options use Material 3 (Chrome 130+ tokens): rounded surfaces, sliding switches, pill-shaped chips, primary blue
#0b57d0, subtle scale + fade entrance. - Firefox popup / options use Acorn / Proton (Firefox 130+ tokens): tighter 4-8 px radii, thin-bordered cards, outlined switches and radio rings, accent
#0061e0/#00ddff.
- Chrome popup / options use Material 3 (Chrome 130+ tokens): rounded surfaces, sliding switches, pill-shaped chips, primary blue
-
Per-site control: click the toolbar icon to toggle the current site. Choose between two default modes:
- Enable on all sites (default) - sites you turn off are remembered as
domain*off. - Disable on all sites - sites you turn on are remembered as
domain*on.
- Enable on all sites (default) - sites you turn off are remembered as
-
Local font: Vazirmatn is bundled with every build. No external request, no CDN dependency.
-
Custom variable font: upload your own
.woff2 / .woff / .ttf / .otffile from settings. Variable fonts are recommended (one file, every weight, smaller, sharper). -
Live editor support: works on inputs, textareas and
contenteditableeditors as you type a single character. Uses the W3Cdir="auto"attribute, never breaks editor internals. -
Built for dynamic apps: ChatGPT, Claude, X.com, Notion, Google services, Gemini - dynamic content gets RTL'd as it streams in.
-
URL-aware bidi: inline
cite / code / kbd / samp / var / preinside RTL blocks keep their own LTR context, so URLs in Google search results no longer read backwards. -
Shadow DOM aware: the content script descends into open shadow roots, so Web-Component-based UIs get the same treatment as light DOM.
-
Performance budget: observer only queues, a debounced flush walks with a 10 ms / 1000-node ceiling, expando flags replace WeakSets, and
all_framesstays off, tagging never blocks the page. -
Diagnostic logging: a debug toggle prints labelled traces to DevTools.
Pre-built bundles for v2.3 live on the Releases page as
dynamic-rtl-chrome-v3.zip,dynamic-rtl-firefox-v3.zipanddynamic-rtl-safari-v3.zip. They are also checked in underresources/for offline access.
- Download
dynamic-rtl-chrome-v3.zipand extract it (for example to~/Extensions/dynamic-rtl-chrome). - Open
chrome://extensions(oredge://extensions,brave://extensions, etc.). - Turn on Developer mode in the top-right corner.
- Click Load unpacked and select the extracted folder.
- The Dynamic RTL icon appears in your toolbar. Pin it for quick access. The icon is colored when active on the current site, grayscale when inactive.
Updating: download the new zip, replace the folder, click the reload icon on the extension card.
Firefox blocks unsigned extensions on the regular release channel. Three supported paths:
Path A: temporary install (any Firefox):
- Download
dynamic-rtl-firefox-v3.zip. - Open
about:debugging#/runtime/this-firefox. - Click Load Temporary Add-on... and select the
manifest.jsonfile inside the (extracted) zip. - The extension stays installed until you restart Firefox.
Path B: permanent install (Developer Edition / Nightly / ESR):
- Open
about:configand setxpinstall.signatures.requiredtofalse. Renamedynamic-rtl-firefox-v3.ziptodynamic-rtl-firefox-v3.xpi. - Drag the
.xpifile into Firefox and click Add.
Path C: regular Firefox (recommended once published): install from https://addons.mozilla.org/ once the build is signed by Mozilla.
Safari 26 can load the extension straight from a folder, no Xcode needed:
- Download
dynamic-rtl-safari-v3.zipand extract it. - Safari → Settings → Developer tab → Add Temporary Extension…, pick the extracted folder, confirm with Touch ID / password.
- Settings → Extensions → enable it with Always Allow. Note: it unloads when Safari quits.
- Click the toolbar icon to open the popup. The icon is colored when Dynamic RTL is active on the current page, and grayscale when it isn't.
- The toggle at the top enables / disables RTL on the current site. Each toggle is recorded as an explicit override (
domain*onordomain*off) in the custom site list. - Use the segmented control to flip between Enable on all sites and Disable on all sites. This default applies to sites without an explicit entry. Your custom site list is preserved when you switch modes.
- Click Settings & custom font to open the full options page. There you can edit the custom site list directly, change fonts, and turn on debug logging.
Every build ships with Vazirmatn: the variable cut, with a continuous wght axis from 100 to 900. To replace it, use the Font section of the settings page in your build of choice and upload a .woff2 / .woff / .ttf / .otf file (up to 8 MB).
Why variable fonts? A single variable font file covers every weight, looks crisp at every size, and uses less memory than shipping nine static cuts.
Special care has been taken to make Dynamic RTL behave on heavy SPAs:
- ChatGPT (
chat.openai.com,chatgpt.com): streamed message tokens, ProseMirror composer. - Claude (
claude.ai): streamed conversations, prompt editor. - Gemini (
gemini.google.com): composer + streamed responses. - X / Twitter (
x.com,twitter.com): virtualised timelines, replies, the composer. - Notion: per-block contenteditable, slash menu.
- Google services: Gmail composer (contenteditable), Calendar, Search, YouTube comments. URL / breadcrumb fragments inside RTL search results stay in LTR via
unicode-bidi: isolate. - Google Docs / Sheets / Slides: the canvas-rendered document area is intentionally skipped; comments, sidebars and menus are styled normally.
- Telegram Web, WhatsApp Web, Discord, Slack, Reddit, Stack Overflow, GitHub, app.kiro.dev: covered by the generic mutation observer + shadow-DOM walker.
If you hit a site that misbehaves, reload the page, open DevTools and check the console for errors for the bug report.
- Tagging is done by adding a single CSS class, never inline styles, so style recalculation is fast and reversible.
- Touched elements carry a tiny expando flag, so already-tagged subtrees bail out in a few pointer hops with zero GC pressure.
- The observer callback only queues dirty roots; a debounced flush walks them with a 10 ms / 1000-node budget, then yields the thread.
- While the page is still parsing, the first flush runs pre-paint (no idle wait), so pages render with RTL already applied; idle scheduling only applies after load.
- The text walk is a single
TreeWalkerpass that also discovers open shadow roots and rejects<script>,<style>,<code>,<pre>, canvas editors and contenteditable subtrees. all_framesis off, so ads and iframes cost nothing.- The font uses a
unicode-rangelimited to Arabic-script blocks, so English-only pages never download it.
Dynamic RTL does not call the network. Ever.
- The Vazirmatn font is bundled inside every build.
- Per-site list, custom font, and other preferences live in
chrome.storage.localon your machine. - There is no telemetry, no analytics, and no remote configuration.
The browser builds request storage, tabs and activeTab permission, plus host permission for http://*/* and https://*/*. The host permission is required because the extension needs to run a content script on every site you visit to detect Persian / Arabic text.
The content script stays silent by design. If something looks wrong, work through these:
Common fixes:
| Symptom | Try |
|---|---|
| Font does not change but direction is correct | Make sure the toolbar icon is colored (active). Reload the page once to give the font cache a chance. |
| A specific site looks wrong | Click the toolbar icon to toggle it off. The site is recorded in your custom site list. |
| Custom font does not load | The file must be .woff2, .woff, .ttf or .otf and smaller than 8 MB. Variable fonts must include the wght axis. |
| Nothing happens at all | Open the options page, check the default mode and the custom site list. Then open DevTools and look for errors. |
.
+-- chrome/ Chrome MV3 build (use this for Chromium browsers)
| +-- manifest.json
| +-- background/service-worker.js
| +-- content/ early-inject.js, main.js
| +-- popup/ popup.html / popup.css / popup.js (Material 3)
| +-- options/ options.html / options.css / options.js
| +-- fonts/ Vazirmatn-Variable.woff2
| +-- icons/ active + inactive icons (16 / 32 / 48 / 128)
+-- firefox/ Firefox MV3 build (Acorn / Proton styling)
+-- safari/ Safari MV3 build (same code as chrome/, load as Temporary Extension)
+-- resources/ Pre-built zip packages for all three builds
+-- scripts/build-zips.sh Re-create the release packages
+-- LICENSE
+-- README.md
You do not need a build step. To recreate the release zips:
./scripts/build-zips.shThis produces:
resources/dynamic-rtl-chrome-v3.zip
resources/dynamic-rtl-firefox-v3.zip
resources/dynamic-rtl-safari-v3.zip
Dynamic RTL is released under the MIT License. The bundled Vazirmatn font is licensed under the SIL Open Font License 1.1.