How to rtl svg icon system for multilingual apps
- Step 1Lay out the icon folders — Create
src/icons/ltr/(the source of truth) andsrc/icons/rtl/. All authored icons live inltr/. After generation, the Mirror-bucket icons appear inrtl/with the same base name. Keep-bucket icons are NOT copied — the loader falls back toltr/for them. - Step 2Maintain a direction manifest — Keep a manifest marking each icon mirror/keep/redesign. The flip tool has no classifier, so this manifest is the single source of what gets flipped. Generate
rtl/only from the mirror entries. - Step 3Generate RTL variants deterministically — Run each mirror-bucket icon through svg-rtl-mirror (browser for one-offs, runner for the batch). Because path data is untouched, the output is reproducible — safe to commit and cache.
- Step 4Build the locale-aware loader — Write an Icon component that reads the current direction (
document.dir, an i18n context, orI18nManager.isRTLin React Native) and resolves the asset fromrtl/when present, elseltr/. This keeps non-directional icons single-sourced automatically. - Step 5Handle text and animation cases — Outline any icon with live
<text>via svg-font-to-path before flipping (or keep it LTR). Review SMIL/CSS animations on flipped icons — a left-to-right slide becomes right-to-left after the mirror and may need its offsets revisited. - Step 6Automate in CI and commit the output — Add generation as a CI step triggered when
ltr/changes, and commit thertl/outputs so the runtime loader always has static files. Cache the step by a hash of the source icons to skip unchanged runs.
System architecture at a glance
Three layers; the flip tool only powers the generation layer.
| Layer | Responsibility | Owns the decision? |
|---|---|---|
| Classification (manifest) | Marks each icon mirror / keep / redesign | You — the tool has no classifier |
| Generation (svg-rtl-mirror) | Flips mirror-bucket icons deterministically | Tool — geometry only |
| Loader (runtime) | Resolves rtl/ else ltr/ per locale direction | Your app code |
Direction handling across platforms
How each runtime should select the right variant. Pre-generated files behave identically everywhere; CSS transforms do not.
| Platform | Direction signal | Recommended source |
|---|---|---|
| Web (inline/img) | document.dir / CSS [dir='rtl'] | CSS flip OR baked rtl/ files |
Web (sprite <use>) | document.dir | Baked rtl/ sprite |
| React Native | I18nManager.isRTL | Baked rtl/ files (CSS transform unreliable) |
| Flutter | Directionality.of(context) | Baked rtl/ files |
| HTML email | dir attribute (limited support) | Baked rtl/ files (transforms unsupported) |
Cookbook
Concrete folder layout and loader code. The flip step is deterministic, so everything downstream stays reproducible.
Folder layout with single-sourced Keep icons
Only directional icons get an rtl/ file. The loader falls back to ltr/ for everything else, so non-directional icons aren't duplicated.
src/icons/
ltr/
arrow-back.svg ← directional (will be flipped)
logo.svg ← keep (NOT copied to rtl/)
search.svg ← keep
rtl/
arrow-back.svg ← generated: scale(-1,1) group added
manifest.json
{ "arrow-back": "mirror", "logo": "keep", "search": "keep" }Locale-aware loader (React, web)
Resolve rtl/ when a variant exists, else fall back to ltr/. No per-icon CSS needed.
import { useContext } from "react";
import { DirCtx } from "./i18n"; // "ltr" | "rtl"
const rtlSet = import.meta.glob("./icons/rtl/*.svg", { as: "url", eager: true });
const ltrSet = import.meta.glob("./icons/ltr/*.svg", { as: "url", eager: true });
export function Icon({ name }: { name: string }) {
const dir = useContext(DirCtx);
const rtlUrl = rtlSet[`./icons/rtl/${name}.svg`];
const url = dir === "rtl" && rtlUrl
? rtlUrl // directional → flipped file
: ltrSet[`./icons/ltr/${name}.svg`]; // keep → single source
return <img src={url} alt="" width={24} height={24} />;
}React Native uses I18nManager, not CSS
Native can't apply CSS scaleX(-1) reliably, so the static-file approach is the portable one.
import { I18nManager } from "react-native";
import ArrowBackLtr from "./icons/ltr/arrow-back.svg";
import ArrowBackRtl from "./icons/rtl/arrow-back.svg";
export const ArrowBack = () =>
I18nManager.isRTL ? <ArrowBackRtl/> : <ArrowBackLtr/>;
// Pre-generated files render identically on web + native.
// No transform: scaleX(-1) needed — and it wouldn't apply here.CI generation step (deterministic, cached)
Regenerate rtl/ only when ltr/ changes; the deterministic flip keeps committed output stable.
.github step:
- name: Generate RTL icons
run: npm run generate-rtl # runner-backed batch flip
- name: Fail if rtl/ drifted
run: git diff --exit-code src/icons/rtl/
# Because the flip never rewrites path data, an unchanged
# ltr/ icon produces a byte-identical rtl/ file → no diff.Animated icon needs a direction review
Mirroring flips motion direction too. Verify any animated SVG after the flip.
Before (LTR): a chevron slides left→right on hover.
After (RTL flip): the same animation now slides right→left,
which may or may not be what you want.
Action: review SMIL <animateTransform> / CSS keyframe offsets
on each animated icon post-flip; the tool only mirrors the
static geometry, not your intent for the motion.Edge cases and what actually happens
Treating the flipper as a classifier
Not supportedThe tool flips whatever it's given — it has no mirror/keep/redesign logic. Your manifest is the authority on what gets flipped. Generate rtl/ only from mirror-bucket entries.
Duplicating non-directional icons into rtl/
Drift riskCopying Keep-bucket icons into rtl/ creates two files that can diverge. Single-source them: let the loader fall back to ltr/. Only directional icons need an rtl/ file.
Relying on CSS scaleX(-1) on native
Won't applyReact Native and Flutter don't apply CSS transforms to icons the way the web does. Use pre-generated rtl/ files driven by I18nManager.isRTL / Directionality.of(context) for portable behaviour.
Animated icons after the flip
Review motionMirroring reverses the direction of any SMIL/CSS animation baked into the icon. A left-to-right slide becomes right-to-left. The tool only mirrors static geometry — re-check each animated icon's offsets manually.
Icons with embedded labels
Renders reversedLive <text> flips into backwards glyphs. Outline with svg-font-to-path before flipping, or keep such icons LTR and re-typeset the label per locale.
Inconsistent viewBoxes across the set
Fallback W=24Icons without a viewBox flip with the default W = 24 and may land off-frame if they're on another grid. Normalise the whole set's viewBoxes (e.g. via svg-viewbox-fixer) before generating RTL variants.
Sprite-based delivery
Rebuild requiredThere's no in-place sprite flip. Split the sprite, flip the directional symbols, and rebuild a separate icons-rtl sprite with svg-sprite-builder; reference the sprite matching the page direction.
Committed rtl/ files keep changing in CI
Check the pipelineThe flip is deterministic, so an unchanged source should yield an identical file. If rtl/ keeps drifting, something else in your pipeline is re-formatting the SVGs (a minifier, a formatter) between runs. Pin the order: flip, then optionally minify once, then commit.
Free-tier developer
Pro requiredRTL Mirror is Pro-tier. A free account can't run the generation step. Upgrade to Pro to enable both the browser tool and runner-backed batch generation.
Frequently asked questions
Should RTL icons live in the same repo as LTR icons?
For most teams, yes. Commit pre-generated static files for the simplest, fastest runtime. The cost is a slightly larger repo. The flip is deterministic (path data untouched), so committed RTL files only change when the LTR source does — diffs stay clean.
How do I keep the two sets in sync?
Treat ltr/ as the source of truth and regenerate rtl/ in CI whenever ltr/ changes, then commit. A git diff --exit-code on rtl/ catches anyone who edited a flipped file by hand. Because the flip is reproducible, this stays stable.
Do I need a separate file for every icon in RTL?
No — only directional (mirror-bucket) icons. Single-source the Keep bucket: let your loader fall back to ltr/ when no rtl/ variant exists. This avoids duplicating logos, search, gears, etc.
How do I handle RTL in React Native?
Drive selection from I18nManager.isRTL and import the pre-generated rtl/ file for directional icons. CSS scaleX(-1) isn't a reliable cross-platform mechanism, so static files are the portable choice — and they render identically on web.
What about Flutter?
Use Directionality.of(context) to pick the variant, again from pre-generated files. The same deterministic flip output works across web, React Native and Flutter because it's just a standard SVG with a transform group.
Do animated icons need special handling?
Yes. Mirroring reverses the direction of any baked SMIL/CSS animation — a left-to-right motion becomes right-to-left. The tool only mirrors static geometry, so review each animated icon's offsets after flipping.
Can the tool flip a sprite sheet in place?
No. Split the sheet into standalone icons, flip the directional ones, then rebuild a separate RTL sprite with svg-sprite-builder. The flipper works on one whole SVG at a time.
What about icons that contain text labels?
Flipping reverses live <text>. Convert to outlines with svg-font-to-path before flipping, or keep them LTR and re-typeset the label per locale — usually the better call for text-bearing icons.
Does the runtime loader add overhead?
No meaningful runtime flip cost — the variants are static files, so the loader just picks a URL based on direction. There's no on-the-fly transform, which is the whole point of pre-generating.
How do I manage parallel variants in design tools?
Keep LTR as the canonical artboard set and generate RTL from it rather than hand-flipping in the design tool. Export the LTR icons, run them through the flip pipeline, and treat rtl/ as build output — not something designers edit directly.
What plan is required?
RTL Mirror is Pro-tier. Free accounts see an upgrade prompt. Pro covers both the in-browser tool and the runner-backed batch generation your CI uses; the per-file SVG limit on Pro is 50 MB, far beyond any icon.
Should I minify the flipped files before committing them?
Optionally. The flip leaves a small <g transform=…> wrapper; running the output once through svg-pro-minifier can collapse it and shrink the file. If you do, pin the order — flip, then minify, then commit — and run minification deterministically, so committed rtl/ files don't drift between builds.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.