Skip to content

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 direction
npx newtui@latest add direction
yarn dlx newtui@latest add direction
bunx newtui@latest add direction
import { 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.