Direction
FreeSets reading direction for everything inside it. Spacing, borders, affixes and the selected edge follow on their own; icons that point somewhere flip when marked.
pnpm dlx shadcn@latest add https://caliperui.site/r/direction.jsonWhen to use #
Use it for
- Arabic, Hebrew, Persian and Urdu interfaces.
- A preview that shows a layout in both directions.
Skip it when
- Flipping numbers, logos or media controls; they keep their direction.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Logical properties, not mirrored styles
- Parts use inline-start and inline-end and :dir() rules, so one stylesheet serves both directions. Mark a chevron or back arrow with data-mirror.
Usage
- Overlays read the direction
- Dialogs render in a portal outside the wrapper, so they take it from useDirection. The alert dialog does this for you.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/direction.json
Adds the file to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from Direction and useDirection.
import { ChevronRight, FileText } from "lucide-react" import { useState } from "react" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { Direction, type TextDirection } from "@/components/ui/direction" import { Input } from "@/components/ui/input" import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@/components/ui/item" import { Label } from "@/components/ui/label" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle" export function DirectionDemo() { const [dir, setDir] = useState<string[]>(["rtl"]); const rtl = dir[0] === "rtl"; return ( <div className="grid w-full max-w-sm gap-4"> <ToggleGroup type="single" label="Direction" value={dir} onValueChange={setDir} className="justify-self-start"> <ToggleGroupItem value="ltr" size="sm">LTR</ToggleGroupItem> <ToggleGroupItem value="rtl" size="sm">RTL</ToggleGroupItem> </ToggleGroup> <div className="grid gap-4 rounded-surface border border-border bg-elevated p-4"> <Direction dir={(rtl ? "rtl" : "ltr") satisfies TextDirection}> <div className="flex items-center justify-between gap-3 font-medium"> <span>{rtl ? "ملخص الفوترة" : "Billing summary"}</span> <Badge variant="success">{rtl ? "مدفوعة" : "Paid"}</Badge> </div> <Input aria-label={rtl ? "المبلغ" : "Amount"} defaultValue="4,820.00" leading="$" trailing="USD" /> <div className="flex items-center"> <Checkbox id="direction-receipt" defaultChecked /> <Label htmlFor="direction-receipt">{rtl ? "أرسل لي الإيصال" : "Email me the receipt"}</Label> </div> <Item asChild> <a href="#direction"> <ItemMedia><FileText aria-hidden="true" /></ItemMedia> <ItemContent> <ItemTitle>{rtl ? "فاتورة أكتوبر" : "October invoice"}</ItemTitle> <ItemDescription>{rtl ? "أُرسلت في ١ أكتوبر" : "Sent 1 Oct"}</ItemDescription> </ItemContent> <ChevronRight data-mirror="" aria-hidden="true" className="size-4 text-muted-foreground" /> </a> </Item> </Direction> </div> </div> ); }
API reference #
Direction
component
Also accepts the native props of its root element and a ref.
useDirection
component
No props of its own; it re-exports a primitive and takes that primitive’s props.
Accessibility #
It is built on native elements, so roles and focus come from the browser.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.