Skip to Content
GuidesLocalisationBidirectional Components

Bidirectional Components

Some components need explicit logic to work correctly in both LTR and RTL layouts. This guide covers the most common patterns.

Horizontal navigation should flow in the reading direction — left-to-right for LTR languages, right-to-left for RTL.

.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 flipShould NOT flip
Arrows (→ ← ↗)Play/pause buttons
Chevrons (‹ ›)Checkmarks
Breadcrumb separatorsUpload/download icons
”Back” / “Forward” arrowsSearch icon
List indent markersPlus/minus icons
.directionalIcon { display: inline-block; transition: transform 0.2s; } :global([dir="rtl"]) .directionalIcon { transform: scaleX(-1); }

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.

Last updated on