# Right to Left

_Box Kite 2.2.5 · a markdown copy of https://www.box-kite.dev/rtl/_

One dir attribute and the whole page mirrors: the sides are logical, the arrow keys follow the reading order, and the components come with it. No second stylesheet, and nothing re-renders.

## The direction is one attribute, and the browser does the rest

`dir` is an attribute, so it goes in `props` — on the `<html>` element for a whole locale, or on any subtree for one panel of it. Everything below is inside a single wrapper carrying it, and the switch on this page changes nothing else: no state reaches a style, no class is rebuilt, no component is told. The logical props — `ps`/`pe`, `ms`/`me`, `bs`/`be`, `insetStart`/`insetEnd`, `borderRadiusStart`/`borderRadiusEnd` — are resolved by the browser from that one attribute, which is why a translation costs no CSS.

every demo on this page is inside it

**A card that reads both ways**

```jsx
import { ArrowRight } from 'lucide-react';

<Flex props={{ dir }} ai="center" gap={3} ps={4} pe={3} py={3} bs={4} borderStyle="solid" borderColor="indigo-500" borderRadiusEnd={2}>
  <Box flex1 fontSize={14}>
    Your order is on its way
  </Box>
  {/* The one thing a logical property cannot say: which way the arrow points. */}
  <Box color="indigo-500" rtl={{ flip: 'xAxis' }}>
    <Icon size={5}>
      <ArrowRight />
    </Icon>
  </Box>
</Flex>
```

## The form controls mirror, including the parts the page does not draw

A field's icon, the room made for it, the gap between a checkbox and its label: all of it is declared on the inline axis, so all of it swaps. The interesting one is the switch. Its thumb is a `::before` that _travels_, and travel is a `translateX` — the one thing no logical property can express. So the built-in style says the distance once and `rtl` says it again with the other sign, which is what the two direction keys are for.

**Fields, boxes and switches**

```jsx
import { Search } from 'lucide-react';

<Flex props={{ dir }} d="column" gap={4}>
  <Box position="relative" width={70}>
    {/* Both the icon and the room made for it are on the inline axis, so both swap. */}
    <Flex position="absolute" insetStart={3} top="1/2" translateY="-1/2" color="slate-400" pointerEvents="none">
      <Icon size={4}>
        <Search />
      </Icon>
    </Flex>
    <Textbox placeholder="Search invoices" ps={10} width="fit" />
  </Box>
  <Checkbox label="Email me a copy" defaultChecked />
  <Switch label="Send reminders" defaultChecked />
  <RadioGroup label="Currency" defaultValue="usd" orientation="horizontal">
    <RadioGroup.Item value="usd" label="USD" />
    <RadioGroup.Item value="egp" label="EGP" />
    <RadioGroup.Item value="ils" label="ILS" />
  </RadioGroup>
</Flex>
```

## The arrow keys follow the reading order, not the screen

This is the half no prop can fix. APG says the arrows move in the reading order, so in a right-to-left list `ArrowLeft` goes to the _next_ item and `ArrowRight` goes back. Nothing in the markup says which: `useRovingFocus` asks the element for its _resolved_ direction the moment a sideways arrow arrives, so a `dir` anywhere above it is enough and a vertical list pays nothing. The radio group above and the grid below both go through it. Tab, Home and End never flip — Home is the first item in the reading order either way.

**Arrow through it in both directions**

```jsx
// Inside useRovingFocus: the direction is a state of the element, so it is read, not configured.
const forward = (event.key === 'ArrowRight') !== isRtl(event.currentTarget);

move(step(activeIndex, forward ? 1 : -1, count, loop, isDisabled), 'keyboard');
```

## A popup keeps the direction it was declared in

`Overlay` stays in the subtree it was declared in — it is in the browser's top layer rather than a portal, and a top-layer element inherits normally — so `Tooltip`, the `Dropdown` popup and the grid's column menu simply inherit the direction around them. Only on the no-Popover-API fallback does the layer leave that subtree for a container hanging off the body, where nothing of the declaration site is inherited and a tooltip on an Arabic paragraph would come out reading left to right; there it measures the direction off its anchor and writes it back on as `dir`.

**A tooltip and a dropdown, mirrored**

```jsx
<Flex props={{ dir }} gap={4} ai="center">
  <Dropdown label="Country" defaultValue="eg">
    <Dropdown.Item value="eg">Egypt</Dropdown.Item>
    <Dropdown.Item value="ma">Morocco</Dropdown.Item>
    <Dropdown.Item value="jo">Jordan</Dropdown.Item>
  </Dropdown>
  <Tooltip content="Sends the invoice and marks it open">
    {(trigger) => <Button {...trigger}>Send</Button>}
  </Tooltip>
</Flex>
```

## A pinned column belongs to the reading order too

A column is pinned to the `START` or the `END` of the inline axis, not to the left or the right of the screen — so the invoice number below stays where the reading begins under either direction, and the total stays where it ends. `LEFT` and `RIGHT` are the older spelling and still mean those two. Three more things follow the reading order with it: `align: 'end'` on a numeric column, the resize handle (drag it — it grows the column towards the reading end in both directions), and the column menu, whose _Pin Left_ says _Pin Right_ when that is the side it would pin to. Scroll it sideways: the pinned columns hold their edges.

**A grid with both edges pinned**

```jsx
<Box props={{ dir }}>
  <DataGrid
    data={invoices}
    def={{
      columns: [
        { key: 'id', header: 'Invoice', pin: 'START', width: 120 },
        { key: 'customer', header: 'Customer', width: 200 },
        { key: 'country', header: 'Country', width: 160 },
        { key: 'status', header: 'Status', width: 140 },
        { key: 'items', header: 'Items', width: 120, align: 'end' },
        { key: 'total', header: 'Total', pin: 'END', width: 130, align: 'end' },
      ],
      rowHeight: 40,
      visibleRowsCount: 6,
      sortable: true,
      resizable: true,
    }}
  />
</Box>
```

## What stays physical, and why

Three things are deliberately not mirrored, because the reading order is not what they are about. `Overlay` positions its layer with a transform in _page_ coordinates measured off the anchor, so its box is anchored at the page's own origin in both directions — a measured pixel has no reading order. The grid's loading bar sweeps the same way either way, because a sweep is not a sentence. And `left`, `right`, `pl`, `mr` and the rest of the physical props are still there and still physical: when you mean the screen side — a shadow the light casts, a decoration in a corner — say so.

## Making your own components mirror

- Reach for `ps`/`pe`, `ms`/`me`, `bs`/`be`, `insetStart`/`insetEnd` and `borderRadiusStart`/`borderRadiusEnd` by default; the axis shorthands `px`, `mx` and `insetX` have always been logical.
- `textAlign="start"` rather than `"left"`, and `jc="start"`/`"end"` rather than `"left"`/`"right"`.
- For what is left — a rotation, a translation, a gradient direction — nest it under `rtl`. Remember `ltr` matches a document with no `dir` at all, since left to right is the initial value.
- Read a direction, never store one: `getComputedStyle(el).direction` is the only answer that accounts for a `dir="auto"` or a `<bdi>` above you.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

