Bidirectional Components
Some components need explicit logic to work correctly in both LTR and RTL layouts. This guide covers the most common patterns.
Navigation
Horizontal navigation should flow in the reading direction — left-to-right for LTR languages, right-to-left for RTL.
CSS Approach
.nav {
display: flex;
gap: 1.5rem;
}
:global([dir="rtl"]) .nav {
flex-direction: row-reverse;
}
.navLink {
padding-inline: 0.75rem;
}Sliders and carousels
Carousels that slide horizontally must reverse their direction in RTL:
import { useApp } from '@/hooks/useApp'
export function TestimonialCarousel({ items }) {
const { isRtl } = useApp()
const [current, setCurrent] = useState(0)
const next = () =>
setCurrent(prev => (prev + 1) % items.length)
const prev = () =>
setCurrent(prev => (prev - 1 + items.length) % items.length)
return (
<div className={styles.carousel}>
{/* Swap prev/next buttons in RTL */}
<button onClick={isRtl ? next : prev} aria-label="Previous">
‹
</button>
<div
className={styles.track}
style={{
transform: `translateX(${isRtl ? current * 100 : current * -100}%)`,
}}
>
{items.map((item, i) => (
<div key={i} className={styles.slide}>
{item.content}
</div>
))}
</div>
<button onClick={isRtl ? prev : next} aria-label="Next">
›
</button>
</div>
)
}Forms
Form inputs automatically inherit text alignment from the document direction. However, some form elements need explicit handling:
.formGroup {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.label {
font-weight: 600;
text-align: start; /* Adapts to direction automatically */
}
.input {
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
text-align: start;
}
/* Checkbox and radio alignment */
.checkboxRow {
display: flex;
align-items: center;
gap: 0.5rem;
}
:global([dir="rtl"]) .checkboxRow {
flex-direction: row-reverse;
}Form layout
Two-column form layouts should flip in RTL:
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
/* No special RTL handling needed for grid — it flows naturally */
/* But labels and content within each cell should align correctly */
.submitRow {
display: flex;
justify-content: flex-end;
}
:global([dir="rtl"]) .submitRow {
justify-content: flex-start;
}Icons with directional meaning
Icons that point in a direction should flip in RTL:
| Should flip | Should NOT flip |
|---|---|
| Arrows (→ ← ↗) | Play/pause buttons |
| Chevrons (‹ ›) | Checkmarks |
| Breadcrumb separators | Upload/download icons |
| ”Back” / “Forward” arrows | Search icon |
| List indent markers | Plus/minus icons |
.directionalIcon {
display: inline-block;
transition: transform 0.2s;
}
:global([dir="rtl"]) .directionalIcon {
transform: scaleX(-1);
}Breadcrumbs
Breadcrumb separators should flow in the reading direction:
const { isRtl, localePath, t } = useApp()
const separator = isRtl ? ' ‹ ' : ' › '
return (
<nav className={styles.breadcrumbs}>
<a href={localePath('/')}>{t('home')}</a>
{separator}
<a href={localePath('/services')}>{t('services')}</a>
{separator}
<span>Service Name</span>
</nav>
)Notification badges
Position badges correctly for both directions:
.notificationBadge {
position: absolute;
top: -4px;
inset-inline-end: -4px; /* right in LTR, left in RTL */
min-width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-accent);
color: #fff;
font-size: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
}CSS logical properties like inset-inline-start, inset-inline-end, margin-inline-start, and padding-inline-end are the cleanest way to handle bidirectional layouts. They automatically adapt to the document direction without needing RTL overrides.