Right to left
Arabic works without a code change: the layout, the icons and the digits follow the page.
The rule
Direction is set once, on the html element. Components read it through logical CSS, never through JavaScript.
import { TopClassProvider } from 'topclassui/provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" dir="rtl">
<body>
{/* the provider forwards direction to Base UI and to gestures */}
<TopClassProvider direction="rtl" locale="ar">{children}</TopClassProvider>
</body>
</html>
)
}Logical classes only
Tailwind still ships the physical utilities. This library never uses them; the logical form does the work instead.
Transforms and gestures
A translate, a transform origin, a gradient or a swipe has no logical form in CSS. These multiply by --tc-dir, which is 1 in LTR and -1 in RTL.
[dir="rtl"] { --tc-dir: -1; }
/* the switch thumb */
--tc-slide: calc(1rem * var(--tc-dir));
className="translate-x-(--tc-slide)"
/* object and background positions have no logical form */
className="object-left rtl:object-right"Placement, not side
Every overlay takes a placement instead of a side. start and end resolve to inline-start and inline-end, so a menu opens correctly in both directions.
<PopoverPopup side="left" align="start" />import { PopoverPopup } from 'topclassui/popover'
<PopoverPopup placement="start-top" />
// start -> inline-start, mirrors
// bottom -> bottom, does notType, plurals and digits
Arabic text takes --tc-font-arabic through a [lang|='ar'] rule, so you never swap a font variable. A label with a count is a function of that count.
// labels/ar.ts
const plural = new Intl.PluralRules('ar')
selected: (n, fmt) => {
const rule = plural.select(n)
const value = fmt.format(n)
if (rule === 'zero') return 'لم يتم تحديد شيء'
if (rule === 'one') return 'عنصر واحد محدد'
if (rule === 'two') return 'عنصران محددان'
if (rule === 'few') return `${value} عناصر محددة`
if (rule === 'many') return `${value} عنصرًا محددًا`
return `${value} عنصر محدد`
}
<TopClassProvider direction="rtl" locale="ar" numberingSystem="arab">What the lint rule catches
The rule runs on every class string in the library and fails the lint on a physical class. It catches the deprecated aliases too.
// wrong: one error per class string
className="pl-4 text-left"
// R-5.3-1: "pl-" is physical. use ps- or pe-.
// fix it, and the next run names text-left
// right
className="ps-4 text-start"