Hebrew is the default locale, so most coaches see the app right to left. Four of the six locales are left to right. Every layout decision has to work in both.

Where direction comes from

  1. app/layout.tsx sets dir on html from getLocaleDirection(locale).
  2. HtmlLocaleSync updates lang and dir on the client after a locale switch. See i18n.
  3. Tailwind’s rtl: and ltr: variants and all logical utilities follow the dir attribute.
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 no rtl: override. Examples from the shell:
  • AppWrapper offsets the content with md:ms-68 or md:ms-[76px] and draws the divider with md:border-s.
  • NavBar is md:fixed md:start-0 and 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 lucide ChevronRightIcon 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.
All wrappers accept the normal lucide props and merge 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.
Avoid hand written swaps such as showing one icon with rtl:hidden and another with ltr:hidden. That idiom is where an inverted pager bug hid before the shared Pagination component moved to these wrappers.
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 with dir="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. LoginOtpStep wraps the destination in a span with dir="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.
The reverse also happens: a block of Hebrew legal text in a page that may be rendered in English gets 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

  1. No ml-, mr-, pl-, pr-, left-, right-, text-left or text-right.
  2. Arrows and chevrons come from directional-icon.tsx.
  3. Numbers, phone numbers and codes that must read left to right are wrapped in dir="ltr".
  4. Sheets and drawers use start or end.
  5. Check the screen in Hebrew and in English. The Hebrew check alone hides bugs in the four LTR locales.