RTL
Every component is written with logical properties, so a right-to-left interface is one attribute rather than a second stylesheet.
newt/ui has no left-to-right assumptions to undo. Every component in all three
flavours uses CSS logical properties — margin-inline-start rather than
margin-left, inset-inline-start rather than left, ms-2 rather than
ml-2 — so a right-to-left tree restyles itself.
Get started
Set the direction on the document:
<html lang="ar" dir="rtl"></html>
That is the whole feature for a page that is entirely right-to-left. For a
subtree — a preview, a quoted message, a settings panel in another language —
add the direction component:
pnpm dlx newtui@latest add directionnpx newtui@latest add directionyarn dlx newtui@latest add directionbunx newtui@latest add directionimport { DirectionProvider } from "@/components/ui/direction"
;<DirectionProvider dir="rtl">
<MessageGroup author="ليلى">كل شيء يعمل من اليمين إلى اليسار.</MessageGroup>
</DirectionProvider><script setup lang="ts">
import { DirectionProvider } from "@/components/ui/direction"
</script>
<template>
<DirectionProvider dir="rtl">
<MessageGroup author="ليلى">كل شيء يعمل من اليمين إلى اليسار.</MessageGroup>
</DirectionProvider>
</template>The provider sets dir on its wrapper and exposes the direction through
context, for the few components that have to make a decision the CSS cannot —
which side a panel flips to, which arrow key advances a list.
import { Avatar } from "@/registry/bases/newt/ui/avatar"
import { DirectionProvider } from "@/registry/bases/newt/ui/direction"
import { MessageComposer } from "@/registry/bases/newt/ui/message-composer"
import {
MessageGroup,
MessageGroupAuthor,
MessageGroupAvatar,
MessageGroupBody,
MessageGroupContent,
MessageGroupHeader,
MessageGroupTime,
} from "@/registry/bases/newt/ui/message-group"
/*
* The same components as anywhere else, with one attribute above them. Nothing
* here is direction-aware: the logical properties do the work.
*
* Placeholder names only — no real account, and no Discord asset.
*/
export default function DirectionDemo() {
return (
<DirectionProvider
dir="rtl"
className="flex w-full max-w-md flex-col gap-2"
>
<MessageGroup>
<MessageGroupAvatar>
<Avatar>ل</Avatar>
</MessageGroupAvatar>
<MessageGroupBody>
<MessageGroupHeader>
<MessageGroupAuthor>ليلى</MessageGroupAuthor>
<MessageGroupTime dateTime="2026-09-07T12:04">
اليوم 12:04
</MessageGroupTime>
</MessageGroupHeader>
<MessageGroupContent>
كل شيء يعمل من اليمين إلى اليسار.
</MessageGroupContent>
</MessageGroupBody>
</MessageGroup>
<MessageComposer channelName="عام" />
</DirectionProvider>
)
}<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import { DirectionProvider } from "~~/registry/bases/newt/ui/direction"
import { MessageComposer } from "~~/registry/bases/newt/ui/message-composer"
import {
MessageGroup,
MessageGroupAuthor,
MessageGroupAvatar,
MessageGroupBody,
MessageGroupContent,
MessageGroupHeader,
MessageGroupTime,
} from "~~/registry/bases/newt/ui/message-group"
/*
* The same components as anywhere else, with one attribute above them. Nothing
* here is direction-aware: the logical properties do the work.
*
* Placeholder names only — no real account, and no Discord asset.
*/
</script>
<template>
<DirectionProvider dir="rtl" class="flex w-full max-w-md flex-col gap-2">
<MessageGroup>
<MessageGroupAvatar>
<Avatar>ل</Avatar>
</MessageGroupAvatar>
<MessageGroupBody>
<MessageGroupHeader>
<MessageGroupAuthor>ليلى</MessageGroupAuthor>
<MessageGroupTime date-time="2026-09-07T12:04">
اليوم 12:04
</MessageGroupTime>
</MessageGroupHeader>
<MessageGroupContent>
كل شيء يعمل من اليمين إلى اليسار.
</MessageGroupContent>
</MessageGroupBody>
</MessageGroup>
<MessageComposer channel-name="عام" />
</DirectionProvider>
</template>How it works
Two mechanisms, and only two.
Logical properties. The physical ones do not appear anywhere in the registry. The mapping, if you are converting code of your own:
| Physical | Logical |
| —————————— | —————————–– | ——— | —– | — | —— | ––––– | — | — | — | — | —— |
| margin-left / -right | margin-inline-start / -end |
| padding-left / -right | padding-inline-start / -end |
| border-left / -right | border-inline-start / -end |
| left: / right: | inset-inline-start / -end |
| border-top-left-radius | border-start-start-radius |
| text-align: left / right | text-align: start / end |
| ml-*, mr-*, pl-*, pr-* | ms-*, me-*, ps-*, pe-* |
| left-*, right-* | start-*, end-* |
| rounded-l | r | tl | tr | bl | br-\* | rounded-s | e | ss | se | es | ee-\* |
| border-l | r-\* | border-s | e-\* |
| text-left / text-right | text-start / text-end |
A direction multiplier, for the one thing CSS cannot express logically: the
sign of a sideways transform. tokens.css carries it:
:root {
--newt-dir: 1;
}
[dir="rtl"] {
--newt-dir: -1;
}
A component that moves something sideways multiplies by it rather than branching:
transform: translateX(calc(var(--newt-dir) * 16px));
The switch thumb in form-fields is the worked example.
Fonts
The token stack is --newt-font-sans, which starts at Inter. Inter covers Latin
and Cyrillic but not Arabic or Hebrew, so add a face that does and let the stack
fall through:
:root {
--newt-font-sans:
"Inter", "Noto Sans Arabic", "Noto Sans Hebrew", system-ui, sans-serif;
}
Arabic and Hebrew have taller ascenders than Latin at the same size; if the
lines look cramped, raise line-height on the container rather than changing a
component.
Icons
Most glyphs in the registry are symmetric and need nothing. The ones that point — a reply arrow, a chevron, a send glyph — should mirror:
[dir="rtl"] .my-chevron {
transform: scaleX(-1);
}
In Tailwind that is rtl:-scale-x-100. A clock, a checkmark or a logo must
not mirror: they are not directional, and a reversed checkmark reads as
broken.
Migrating
Components installed before the registry moved to logical properties still carry the physical ones. The CLI rewrites them in place:
npx newtui@latest migrate rtl --dry-run
npx newtui@latest migrate rtl
It walks the ui directory, applies the mapping above, prints every
substitution per file, and writes only without --dry-run. It is idempotent:
running it on already-converted files reports nothing.
One case it flags rather than fixes silently: space-x-* is not
direction-safe on Tailwind v3, so it becomes gap-* — which applies to the
container rather than between children, and is worth looking at.
Afterwards, take a pass through your own markup for the two things the CLI cannot see: physical properties in your stylesheets, and directional icons.
Checking it
Every preview on this site has an LTR/RTL toggle in its corner — flipping it
renders the same component in the other direction, which is the fastest check
there is. In your own project, set dir="rtl" on <html> and walk the pages:
what breaks is almost always a physical property in code that is not from the
registry.