Skip to content

Direction provider

Sets reading direction for components that position popups and sliders.

Level
Utility
Category
Utilities
Adoption
Not used yet
import { DirectionProvider } from "@oration/canon/components/direction";
packages/canon/src/components/direction.tsx
INV-20944 to the next run
import { Button } from "@oration/canon/components/button";import { DirectionProvider } from "@oration/canon/components/direction";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuSub,  DropdownMenuSubContent,  DropdownMenuSubTrigger,  DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { SegmentedControl } from "@oration/canon/components/segmented-control";import { Tabs, TabsList, TabsTrigger } from "@oration/canon/components/tabs";import { toast } from "@oration/canon/components/toast";import { ArrowRightIcon, ChevronDownIcon } from "lucide-react";import * as React from "react";export function Hero() {    const [direction, setDirection] = React.useState<"ltr" | "rtl">("rtl");    const runs = ["Friday, Oct 2", "Tuesday, Oct 6", "Friday, Oct 9"];    return (        <div className="flex w-full max-w-md flex-col gap-4">            <SegmentedControl<"ltr" | "rtl">                label="Reading direction"                value={direction}                onValueChange={setDirection}                options={[                    { value: "ltr", label: "Left to right" },                    { value: "rtl", label: "Right to left" },                ]}            />            <div                dir={direction}                className="flex flex-col gap-4 rounded-xl bg-card p-4 shadow-border"            >                <DirectionProvider direction={direction}>                    <Tabs defaultValue="invoices">                        <TabsList aria-label="Supplier record">                            <TabsTrigger value="invoices">Invoices</TabsTrigger>                            <TabsTrigger value="payments">Payments</TabsTrigger>                            <TabsTrigger value="remittances">                                Remittances                            </TabsTrigger>                        </TabsList>                    </Tabs>                    <div className="flex items-center justify-between gap-3">                        <span className="flex items-center gap-2 text-13 text-foreground">                            <ArrowRightIcon                                aria-hidden="true"                                className="size-4 text-muted-foreground rtl:-scale-x-100"                            />                            INV-20944 to the next run                        </span>                        <DropdownMenu>                            <DropdownMenuTrigger                                render={                                    <Button                                        type="button"                                        variant="outline"                                        size="sm"                                    />                                }                            >                                Actions                                <ChevronDownIcon                                    data-icon="inline-end"                                    aria-hidden="true"                                />                            </DropdownMenuTrigger>                            <DropdownMenuContent align="end" className="w-44">                                <DropdownMenuItem                                    onClick={() =>                                        toast.add({                                            type: "success",                                            title: "INV-20944 approved",                                        })                                    }                                >                                    Approve                                </DropdownMenuItem>                                <DropdownMenuSub>                                    <DropdownMenuSubTrigger>                                        Move to run                                    </DropdownMenuSubTrigger>                                    <DropdownMenuSubContent side="inline-end">                                        {runs.map((run) => (                                            <DropdownMenuItem                                                key={run}                                                onClick={() =>                                                    toast.add({                                                        title: `INV-20944 moved to ${run}`,                                                    })                                                }                                            >                                                {run}                                            </DropdownMenuItem>                                        ))}                                    </DropdownMenuSubContent>                                </DropdownMenuSub>                            </DropdownMenuContent>                        </DropdownMenu>                    </div>                </DirectionProvider>            </div>        </div>    );}

Usage#

DirectionProvider tells Base UI components which way text reads, so arrow-key navigation, logical popup sides (inline-start, inline-end) and scroll area scrollbars follow right-to-left languages. It's a straight re-export of Base UI's provider and useDirection hook. It doesn't set the dir attribute: put dir="rtl" on the same subtree so the browser's layout and logical classes (ms-*, ps-*, start-*, text-start) flip too. Oration ships in English only today and nothing renders it.

When to use

  • Wrapping the app, or a region, when it renders a right-to-left locale such as Arabic or Hebrew, alongside dir="rtl".
  • Previewing a component in RTL while building it, to check its keyboard and popup behavior.
  • Inside a custom component that needs the direction for its own logic, such as which arrow key means next, through useDirection().

When not to use

  • Flipping layout and spacing. That's the dir attribute plus logical classes, which work without the provider. Use Layout
  • A single paragraph of right-to-left text inside an English page, such as a supplier's note in Arabic. Set dir="auto" on that element.
  • Mirroring an icon. Use rtl:-scale-x-100 on direction-bearing icons only. Use Iconography

Examples#

Logical classes

The same markup in both directions. border-s, ps-3, ms-auto and text-start follow the reading side with no provider at all; <bdi> keeps the invoice ID and amount reading left to right.

Left to right

Priya Raman

Approved INV-20931

$18,420.00
Right to left

Priya Raman

Approved INV-20931

$18,420.00
export function LogicalLayout() {    return (        <div className="grid w-full max-w-xl gap-4 sm:grid-cols-2">            {(["ltr", "rtl"] as const).map((dir) => (                <div key={dir} className="flex flex-col gap-1.5">                    <span className="text-xs text-muted-foreground">                        {dir === "ltr" ? "Left to right" : "Right to left"}                    </span>                    <div                        dir={dir}                        className="flex items-center gap-3 rounded-xl bg-card p-3 shadow-border"                    >                        <span                            aria-hidden="true"                            className="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-foreground"                        >                            PR                        </span>                        <div className="min-w-0 flex-1 border-s border-border ps-3 text-start">                            <p className="text-13 font-medium text-foreground">                                Priya Raman                            </p>                            <p className="text-xs text-muted-foreground">                                Approved{" "}                                <bdi className="font-mono">INV-20931</bdi>                            </p>                        </div>                        <span className="ms-auto text-13 text-foreground tabular-nums">                            <bdi>$18,420.00</bdi>                        </span>                    </div>                </div>            ))}        </div>    );}

States#

States
StateTreatment
Left to rightThe default, "ltr".
Right to left"rtl": horizontal arrow keys swap in tabs, toolbars and radio groups, submenus open with ArrowLeft, inline-* popup sides flip, sliders run from the right, and scroll area scrollbars move to the left.

Behavior#

  • direction defaults to "ltr". Nested providers override outer ones for their subtree.
  • Base UI reads it for keyboard navigation (in RTL, ArrowLeft moves to the next tab or item), for logical popup sides and alignment, and for scrollbar placement.
  • Physical props don't flip: a popup with side="right" opens to the right in both directions. Use side="inline-end" for submenus and side panels that should follow reading direction.
  • Popups portal to <body>, outside a dir set on a region, so their text runs left to right unless dir is on <html>. Set it on the document for a real RTL locale.
  • useDirection() returns the nearest provider's direction, or "ltr" with none.

Do and don't#

Do. Set dir and the provider together on the same root, ideally <html dir> plus a provider at the app root.
Don't. Add the provider alone. Keyboard and popup logic flip while the layout stays left to right, so arrows and visuals disagree.
Priya Raman

Approved INV-20931

Do. Use logical classes: ms-2, ps-3, border-s, start-0, text-start.
Priya Raman

Approved INV-20931

Don't. Use physical ones (ml-2, pl-3, border-l, left-0, text-left) for anything that should follow reading direction. They stay put in RTL.

Content#

  • Mirror icons that point along the reading direction (back, forward, next, reply, indent). Don't mirror logos, checkmarks, media controls, clocks or charts.
  • Numbers, amounts, IDs and code stay left to right inside right-to-left text; wrap them in <bdi> or dir="ltr" when they sit next to RTL words.

Accessibility#

  • Pair dir with the right lang on the same element so screen readers pick the right voice and reading order.
  • Under RTL, arrow-key meaning flips in Base UI components; custom keyboard handling should read useDirection() and flip the same way.
  • Focus order follows the DOM, which doesn't change with direction; keep the DOM in logical reading order.
Keyboard interactions
KeysAction
ArrowLeftIn a horizontal Base UI tab list or menu bar: previous item in LTR, next item in RTL.
ArrowRightNext item in LTR, previous item in RTL.

API reference#

DirectionProvider

From @oration/canon/components/direction, re-exported from @base-ui/react/direction-provider.

Other props spread onto Nothing; only these props.

Props of DirectionProvider
PropTypeDefaultDescription
direction"ltr" | "rtl""ltr"The reading direction for Base UI components inside.
childrenReactNodeNo defaultThe subtree.

useDirection

useDirection(): "ltr" | "rtl". Takes no arguments.

No props of its own.

Returns

Props of Returns
PropTypeDefaultDescription
direction"ltr" | "rtl"No defaultThe nearest provider's direction, "ltr" without one.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

Not used anywhere: Oration has no right-to-left locale, so no component has been checked in RTL.

53 of 133 files in packages/canon/src/components use physical spacing or position classes (ml-*, pl-*, left-* and their pairs), which won't flip.

DropdownMenuSubContent defaults to side="right" and the sub-trigger draws ChevronRightIcon with ml-auto, so submenus open and point the wrong way in RTL unless you pass side="inline-end".

useOverflowFade assumes left to right, so scrollable tab rows fade the wrong edges in RTL.