Where direction comes from
app/layout.tsxsetsdironhtmlfromgetLocaleDirection(locale).HtmlLocaleSyncupdateslanganddiron the client after a locale switch. See i18n.- Tailwind’s
rtl:andltr:variants and all logical utilities follow thedirattribute.
ClientProviders also wraps the app in Radix Direction.Provider with dir="rtl" hardcoded. Radix components that read direction from context (for example arrow key navigation in tabs and the opening side of sub menus) therefore behave as right to left in every locale. That is correct for Hebrew and Arabic and wrong for the others.
Logical CSS only
Use logical utilities. They flip with direction and need nortl: override.
Examples from the shell:
AppWrapperoffsets the content withmd:ms-68ormd:ms-[76px]and draws the divider withmd:border-s.NavBarismd:fixed md:start-0and its toggle sits at-inset-e-[13px], half over the inner edge in either direction.- The dialog close button is
end-4.
NavBar.tsx contains no physical margin or padding classes.
Directional icons
Icons do not flip on their own. A lucideChevronRightIcon points right in both directions, which means “back” in Hebrew. So never import the four physical arrows directly. Use the wrappers in packages/ui/components/directional-icon.tsx.
The names describe reading order, not a physical side. “Forward” is the direction of progress.
className.
Rotation is used for arrows and chevrons, which are symmetric about the horizontal axis. Mirroring (-scale-x-100) is used for icons with internal detail, where a 180 degree rotation would turn them upside down.
When neither wrapper fits, use the variants directly. NavBar’s toggle uses a ChevronDownIcon with ltr:-rotate-90 rtl:rotate-90 when collapsed and the opposite when expanded, so it always points where a click will move the sidebar.
Sheets
packages/ui/components/sheet.tsx has four physical sides (top, bottom, left, right) and two logical ones:
The default is still
right. The mobile navigation drawer uses side="start" so it opens from the same edge as its trigger. Prefer start and end for new sheets unless the side is fixed on purpose.
Left to right islands
Some content must not flip. Pin it withdir="ltr" on the smallest element that needs it. There are a little over a hundred dir= attributes in app and modules. Typical cases:
- Phone numbers, OTP destinations, emails and URLs.
LoginOtpStepwraps the destination in aspanwithdir="ltr". - Numeric inputs and time values such as rest timers.
- Charts and trend lines whose x axis is time.
- The “Powered by” line in the sidebar footer.
- Code like values: slugs, API keys.
dir="rtl". The privacy and support pages carry a dir per document.
Global exceptions
globals.css mirrors the nprogress bar under [dir="rtl"] with transform: scaleX(-1), so it fills from the reading start.
The Logo component in packages/ui/components/logo.tsx is an inline SVG with no direction dependent transform. Logos never mirror.
Mobile previews inside the dashboard
The phone previews drawn in the dashboard (ClientAppPhoneFrame and the *AppPreview.tsx components) render inside the page’s direction. They paint the trainee app, which has its own direction rules in the mobile repo. Treat those previews as approximations.
Checklist for new UI
- No
ml-,mr-,pl-,pr-,left-,right-,text-leftortext-right. - Arrows and chevrons come from
directional-icon.tsx. - Numbers, phone numbers and codes that must read left to right are wrapped in
dir="ltr". - Sheets and drawers use
startorend. - Check the screen in Hebrew and in English. The Hebrew check alone hides bugs in the four LTR locales.