Borders and border radius in RTL
Learn how to mirror one-sided borders and asymmetric corner radii in RTL with logical border and border-radius properties.
Guide15 min read
A card with four equal rounded corners is already fine in RTL. A card with a stripe on the left and rounded corners on the left is not. The stripe stays left, the corners stay left, and the Arabic content begins on the right looking as though it has arrived at somebody else's component.
The fix is not to replace every left with right. It is to describe what the border means. If it marks the reading start, use border-inline-start. If a corner joins the block start and inline start edges, use border-start-start-radius. If the design truly means the physical left side, keep border-left.
That distinction handles almost the whole subject. The remaining difficulty is remembering which of the two start words names which axis. CSS offered no mnemonic, perhaps because the committee had already suffered enough.
Physical borders and logical borders#
Physical border properties name a side of the screen or page: top, right, bottom or left. Logical border properties name a side of the element's text flow:
- block start is where blocks begin, usually the top in horizontal writing;
- block end is where blocks finish, usually the bottom;
- inline start is where a line of text begins, left in LTR and right in RTL;
- inline end is where that line finishes, right in LTR and left in RTL.
For the horizontal writing mode used by most English and Arabic interfaces, the side mapping is:
| Logical property | LTR physical side | RTL physical side |
|---|---|---|
border-block-start | Top | Top |
border-block-end | Bottom | Bottom |
border-inline-start | Left | Right |
border-inline-end | Right | Left |
The CSS Logical Properties specification defines these as flow-relative border shorthands. Their mapping depends on the element's writing-mode, direction and text-orientation. MDN's border-inline-start reference also makes an important detail explicit: it is a shorthand for the start-side border width, style and color.
That means this declaration is complete:
.field-alert {
border-inline-start: 0.375rem solid #b54708;
}In an LTR element it produces a left border. In an RTL element it produces a right border. You do not need a second selector to swap it.
The other logical shorthands behave similarly:
.box {
border-block-start: 1px solid #d0d5dd;
border-block-end: 1px solid #d0d5dd;
border-inline-start: 0.25rem solid #b54708;
border-inline-end: 0;
}Use border-block to set both block edges to the same border and border-inline to set both inline edges. border-inline is not another spelling of border-inline-start; it affects start and end together. Small difference, sizeable surprise.
.row {
border-block: 1px solid #d0d5dd;
border-inline: 0;
}Logical borders are about writing flow, not Arabic as a special case. In a vertical writing mode, inline start may map to a physical top or bottom edge. If your product supports vertical text, use the definition rather than memorising the horizontal table. Tables are loyal only to the assumptions printed above them.
Fix a start-side alert stripe#
Consider a field-monitoring product that shows an alert beside a trail report. The design puts a thicker border on the reading-start side and rounds the two corners on that same side.
Problem
<article class="field-alert">
<p class="field-alert__label">تنبيه ميداني</p>
<h2>إغلاق المسار بسبب ارتفاع منسوب المياه</h2>
<p>أُرسلت فرقة الفحص إلى البوابة الشمالية.</p>
</article>.field-alert {
border: 1px solid #d0d5dd;
border-left: 0.375rem solid #b54708;
border-radius: 0.875rem 0 0 0.875rem;
padding: 1rem;
}The LTR result has a thick left edge, a rounded top-left corner and a rounded bottom-left corner. That is the intended start-side treatment. Under RTL, none of those physical declarations changes meaning. The stripe and rounded corners remain on the left, which is now inline end.
The browser has not failed to notice dir="rtl". border-left, border-top-left-radius and the four positions inside border-radius are physical by definition. Asking them to move would be like expecting background-position: left to develop cultural awareness.
You can patch the Arabic route with an override:
[dir="rtl"] .field-alert {
border-right: 0.375rem solid #b54708;
border-left: 1px solid #d0d5dd;
border-radius: 0 0.875rem 0.875rem 0;
}This can render the intended result, but it makes two selectors responsible for one design rule. Any later change to the width, color or radius must reach both. Nested direction changes also require care because an element can have a different direction from the document.
Better
.field-alert {
border: 1px solid #d0d5dd;
border-inline-start: 0.375rem solid #b54708;
border-start-start-radius: 0.875rem;
border-end-start-radius: 0.875rem;
padding: 1rem;
}The base border gives every edge the thin neutral rule. The later border-inline-start replaces the width and color only on the reading-start edge. In LTR, the component looks the same as before. In RTL, the thicker border maps to the right.
The two logical radius declarations round the corners that touch inline start. In LTR those are top-left and bottom-left. In RTL they are top-right and bottom-right. No direction selector is needed.
This is the kind of physical property R030 reports when it is direction-sensitive and has no RTL override. The report can identify the physical assumption. Your component design still has to answer whether the decorated edge means start, end or a fixed physical side.
Read logical corner names in two steps#
Logical corner names look repetitive until you split them by axis. In border-start-end-radius:
- the first word after
borderis the block side; - the second word is the inline side.
So border-start-end-radius is the corner where block start meets inline end. In a normal horizontal LTR box, that is top-right. In RTL, inline end moves left, so the same property maps to top-left.
The full horizontal mapping is:
| Logical radius | Corner it describes | LTR mapping | RTL mapping |
|---|---|---|---|
border-start-start-radius | Block start + inline start | Top-left | Top-right |
border-start-end-radius | Block start + inline end | Top-right | Top-left |
border-end-start-radius | Block end + inline start | Bottom-left | Bottom-right |
border-end-end-radius | Block end + inline end | Bottom-right | Bottom-left |
The logical corner properties in the CSS specification map according to the element's own writing mode and direction. MDN's border-start-end-radius page describes the same corner as the meeting point of block start and inline end.
A useful reading habit is to say the two axes aloud, at least mentally:
start-start: top side, reading-start side;start-end: top side, reading-end side;end-start: bottom side, reading-start side;end-end: bottom side, reading-end side.
“Top” and “bottom” in that list assume horizontal writing. The axis names are the actual contract. That is why the logical properties also work when a writing mode changes, while a lovingly maintained set of left-and-right overrides does not.
To round both corners on inline start, combine:
.start-rounded {
border-start-start-radius: 1rem;
border-end-start-radius: 1rem;
}To round both corners on inline end, combine:
.end-rounded {
border-start-end-radius: 1rem;
border-end-end-radius: 1rem;
}There is no Level 1 shorthand that combines those two established logical corner properties. Writing both is repetitive but clear. Newer border drafts describe additional side-radius shorthands, but do not adopt them without checking the browsers in your support matrix. A declaration being discussed by a standards group is not the same thing as a declaration reaching your customers.
Not every border radius needs to mirror#
If all four corners have the same radius, keep the ordinary shorthand:
.card {
border-radius: 0.75rem;
}The result is symmetric, so mirroring changes nothing. Replacing four identical corners with four logical longhands would add code without adding meaning. CSS already provides enough opportunities for ceremony.
The physical border-radius shorthand becomes direction-sensitive when its values make the corners unequal. Its positions follow physical corners in clockwise order, starting at top-left. The MDN border-radius reference documents the one-, two-, three- and four-value forms.
/* top-left | top-right | bottom-right | bottom-left */
.card {
border-radius: 1rem 0.25rem 0.5rem 0;
}Those four values do not reverse under RTL. Two- and three-value forms also remain tied to physical corner pairs. A compact shorthand is not logical merely because it is compact.
There are three sensible choices:
- Keep
border-radiuswhen every corner is the same or the shape must stay physically fixed. - Use the logical corner longhands when the asymmetry follows reading flow.
- Use an explicit RTL override when you must support a browser that lacks the logical corner properties.
Avoid combining a direction-sensitive four-value shorthand with logical corner declarations in the same rule unless you have deliberately worked through the cascade. The shorter code will not compensate for the debugging conversation.
Logical radius properties accept the same basic radius values as physical corner longhands. One value produces a circular corner; two values can produce an elliptical one:
.field-alert {
border-start-start-radius: 1.25rem 0.75rem;
}Percentages remain relative to the corresponding border-box dimensions. The corner moves logically, but the geometry of its horizontal and vertical radii does not swap just because direction changes.
Build joined controls from start to end#
Asymmetric corners often appear in segmented controls. A monitoring view switch may present map, list and detail buttons as one joined shape. The first source item should occupy inline start, the last should occupy inline end, and the borders between them should not double up.
Problem
<div class="view-switch" role="group" aria-label="طريقة العرض">
<button type="button">الخريطة</button>
<button type="button">القائمة</button>
<button type="button">التفاصيل</button>
</div>.view-switch {
display: inline-flex;
}
.view-switch > button {
border: 1px solid #98a2b3;
border-radius: 0;
}
.view-switch > button + button {
border-left: 0;
}
.view-switch > button:first-child {
border-radius: 0.625rem 0 0 0.625rem;
}
.view-switch > button:last-child {
border-radius: 0 0.625rem 0.625rem 0;
}In LTR, every button after the first removes the left border that touches its previous sibling. The first button receives the two left corners; the last receives the two right corners. In RTL, a normal flex row begins on the right, but the physical border removal and corner radii stay where they were. The joined outline develops doubled seams, missing outside edges or both, depending on the surrounding styles. It is an ambitious little control.
Better
.view-switch {
display: inline-flex;
}
.view-switch > button {
border: 1px solid #98a2b3;
border-radius: 0;
padding-block: 0.5rem;
padding-inline: 0.875rem;
}
.view-switch > button + button {
border-inline-start: 0;
}
.view-switch > button:first-child {
border-start-start-radius: 0.625rem;
border-end-start-radius: 0.625rem;
}
.view-switch > button:last-child {
border-start-end-radius: 0.625rem;
border-end-end-radius: 0.625rem;
}With normal flex flow, source order follows inline flow. The adjacent-sibling rule removes the border at each later button's inline-start edge. The first source button gets both start corners, and the last gets both end corners. LTR remains unchanged; RTL mirrors the group as one shape.
This pattern assumes you have not reversed visual order with flex-direction: row-reverse or the order property. :first-child and :last-child follow DOM position, not the position produced by visual reordering. If the first DOM item has been moved to the visual end, logical corners cannot rescue the component's source-order model. Keep the DOM and visual sequence aligned unless the interaction has a well-tested reason not to.
Also test the active state. A thicker active border can change the group's total size or make one seam appear darker. One option is to keep border width constant and change color or background. Another is to use an inset effect that does not alter layout. This is a component decision, not an RTL rule, but direction changes are very good at revealing seams everyone had politely ignored.
The element's direction controls the mapping#
Logical borders map from the computed direction of the element they style, not from a permanent page-wide setting. Direction is inherited, so most components follow the document. A nested dir="ltr" changes the mapping for that subtree.
Suppose the Arabic field alert contains an English method note. The orange stripe belongs to the Arabic interface and should stay on the interface's reading-start edge. Put the directional decoration on the outer component, then set the inner text direction separately:
<aside class="method-note">
<p class="method-note__label">ملاحظة من المختبر</p>
<p class="method-note__text" lang="en" dir="ltr">
Review the turbidity sample before reopening the route.
</p>
</aside>.method-note {
border-inline-start: 0.25rem solid #b54708;
border-start-start-radius: 0.75rem;
border-end-start-radius: 0.75rem;
padding-inline-start: 1rem;
}On an RTL page, .method-note inherits RTL, so its start edge is right. The inner paragraph is LTR for correct English text layout, but that does not move a border declared on the parent.
If the border were placed on .method-note__text, its own dir="ltr" would make inline start map left. That may be right when the border belongs to the quoted English content. It is wrong when the border belongs to the surrounding Arabic component.
The practical rule is to place a logical decoration on the box whose reading flow gives that decoration meaning. Use a wrapper when placement and content direction need to differ. The same split is useful for padding, absolute anchors and other logical properties. The HTML direction guide explains how to establish direction at the correct boundary; the border then follows it.
Do not force an entire component to LTR merely to keep one English paragraph readable. Direction affects flex and grid flow, alignment, logical spacing, border sides and corner mapping. Change the smallest text container that needs the different direction. A broad fix produces broad consequences, an arrangement CSS accepts without complaint.
Mixing physical and logical properties changes the cascade#
Logical and physical properties are not independent layers. For a given writing mode and direction, a logical side maps to a physical side, and declarations for that pair participate in the cascade together. The specification describes them as distinct specified properties that share a computed value once the mapping is known.
Consider this rule:
.field-alert {
border-inline-start: 0.375rem solid #b54708;
border-left: 0;
}In LTR, border-inline-start maps to the left. The later border-left declaration wins on that side, so the accent disappears. In RTL, inline start maps to the right. border-left: 0 affects the other side, so the right accent remains.
The source order appears identical. Its collision changes with direction because the mapped pair changes.
This is why a migration that merely appends logical declarations to an old physical stylesheet can behave differently from one component to another. Specificity, cascade layers, source order and !important still apply. Logical properties do not receive diplomatic immunity.
A cleaner rule owns each directional edge in one vocabulary:
.field-alert {
border: 1px solid #d0d5dd;
border-inline-start: 0.375rem solid #b54708;
}Here the broad physical shorthand establishes the same neutral border on all four sides. The later logical shorthand intentionally customises one mapped side. The order is part of the design and should stay visible in the same rule or layer.
Watch for resets elsewhere:
.surface {
border-inline-start: 0.375rem solid #b54708;
}
.dialog .surface {
border: 0;
}The later border: 0 resets every physical border side, including the physical side to which the logical declaration maps. If a border is missing only inside one variant, inspect shorthands and cascade layers before adding another RTL selector.
In DevTools, check:
- the element's computed
directionandwriting-mode; - which physical edge the logical property maps to;
- crossed-out physical and logical declarations for that edge;
- later
border,border-width,border-styleorborder-colorshorthands; - component states such as active, disabled, invalid and selected.
A border can have a nonzero width and still be invisible because its style computes to none. Inspect width, style and color together. Looking only at the color is a pleasant way to spend twenty minutes learning nothing.
Borders made from shadows and pseudo-elements are separate#
Some designs call a box-shadow or a positioned pseudo-element a border because it looks like one. CSS is less sentimental.
A shadow with a horizontal offset does not become logical under RTL. If the shadow conveys direction, it needs a direction-aware counterpart, which is the concern named by R032. If it is merely a light source or elevation cue, mirroring it may be wrong. Decide from meaning, not resemblance.
A pseudo-element stripe has the same positioning problem as any other positioned element:
.field-alert::before {
content: "";
position: absolute;
inset-block: 0;
inset-inline-start: 0;
inline-size: 0.375rem;
background: #b54708;
}That can be useful when the stripe has animation or a shape a normal border cannot produce. For a plain line, border-inline-start is simpler and participates in border geometry without another generated box. Do not hire a pseudo-element for work a border can finish before lunch.
If the pseudo-element follows rounded corners, check clipping deliberately. border-radius rounds the element's border and background, but a child or generated box can still extend beyond the rounded shape unless the component clips overflow or gives the decoration matching radii. Adding overflow: hidden may clip menus, focus rings or other intentional overflow. Solve the actual decoration problem instead of applying a blanket crop to the component.
Know when a physical border is correct#
Logical properties are right when the relationship follows text flow. Physical properties remain right when the relationship follows physical space.
Keep a physical side when, for example:
- a map legend marks the west edge of a fixed map;
- a chart border corresponds to the physical left end of its plotting area;
- a split-screen divider must remain attached to the left viewport pane;
- an image treatment is composed around a fixed crop;
- a print ornament belongs to a physical page edge regardless of language.
Use a logical side when the border marks:
- the beginning or end of a reading sequence;
- the leading edge of a notice, quote or list item;
- the outside corners of the first and last controls in an inline group;
- a selected tab's start or end boundary;
- a component edge that swaps with Arabic layout.
Some decorations should not mirror at all. Four equal borders, four equal radii and centered outlines are direction-neutral. Leave them alone. The goal is not a stylesheet containing the maximum possible number of inline words. The goal is for the code to name the relationship the design actually has.
The same judgment applies to brand shapes. An asymmetric logo is not a layout edge. Do not reconstruct it with logical corner radii and congratulate the browser for reversing the trademark.
Audit an existing stylesheet#
Search for declarations that encode a physical side or corner:
border-left
border-right
border-top-left-radius
border-top-right-radius
border-bottom-left-radius
border-bottom-right-radius
border-radius:
box-shadow:
::before
::afterTreat the results as questions, not automatic replacements. For each declaration, record what the edge means:
| Existing pattern | Intended meaning | Likely action |
|---|---|---|
| Equal border on every side | Symmetric outline | Keep border |
border-left as reading-start accent | Inline start | Use border-inline-start |
border-right as trailing divider | Inline end | Use border-inline-end |
border-top between stacked sections | Block start | Keep it or use border-block-start if writing modes matter |
| Unequal physical corner radii | Fixed physical shape | Keep and document |
| Unequal corners that mark first or last item | Flow-relative shape | Use logical radius longhands |
| Offset shadow that conveys direction | Direction-dependent shadow | Add an RTL counterpart after checking intent |
| Positioned pseudo-element used as a stripe | Flow-relative placement | Use logical inset, or replace with a logical border |
Then inspect companion declarations. A start-side border often has matching padding-left; converting the border but not the padding leaves the text cramped in RTL and spacious in LTR. Use padding-inline-start when the space belongs beside the logical start border. The RTL CSS guide covers the wider set of logical spacing and positioning properties.
Remove obsolete [dir="rtl"] swaps only after the logical base rule is verified. An override may also contain a color, width or state correction unrelated to side swapping. Deleting the whole block because one declaration became unnecessary is brisk, decisive and occasionally disastrous.
If your browser support policy includes older engines, check the compatibility tables for each property you use. An explicit physical fallback plus RTL override can still be the right answer for that policy. Keep the fallback in one place and test both paths; otherwise it becomes a small historical reenactment inside every component.
Test the actual component states#
A static card in one direction proves very little. Test the same markup with dir="ltr" and dir="rtl", then verify the relationships the component promises.
| Case | What to verify |
|---|---|
| Start-side alert | Accent is left in LTR and right in RTL |
| End-side divider | Divider is right in LTR and left in RTL |
| Start corners | Top-left and bottom-left in LTR; top-right and bottom-right in RTL |
| Joined control | First DOM item owns start corners; last owns end corners |
| Adjacent buttons | One visible seam between items, with no missing outside border |
| Nested LTR text | Outer Arabic decoration stays at RTL start; inner English text remains LTR |
| Symmetric card | All four corners remain unchanged |
| Fixed physical decoration | It stays on its documented physical side |
Toggle direction on the component boundary, not merely the page text. Then inspect computed styles. For horizontal RTL, a border-inline-start declaration should contribute to the physical right border; border-start-start-radius should contribute to the top-right radius. Repeat in LTR and confirm the original English rendering has not changed.
Exercise selected, focused, disabled, invalid and hover states. State selectors often reintroduce border-left, reset border-radius or apply a broad border shorthand later in the cascade. Check responsive variants too. A mobile rule written months after the base component may know nothing of its logical ambitions.
Use realistic Arabic copy long enough to wrap. Borders do not create text overlap by themselves, but asymmetric corners and clipped wrappers can make long content expose missing padding or an overly small fixed height. Zoom the page and increase text size. Confirm that:
- text does not touch the thick border or rounded corner;
- focus indicators remain visible around the complete control;
- no child background leaks outside a rounded shell;
- popovers and badges are not clipped by an
overflowrule added for corner clipping; - the component does not gain document-level horizontal overflow.
For the joined control, tab through the buttons. The visual start item should also be the first relevant item in DOM order unless the product has a documented reason otherwise. Logical radii can make a reversed visual order look tidy while keyboard order remains confusing. A neat outline is not a waiver from interaction testing.
Finally, test nested direction explicitly. Add an LTR child to the RTL component and decide which box owns each decoration. If a border moves when the inner text direction changes, it is probably attached to the wrong box. That single test catches a subtle class of bugs before it becomes an argument about whether “start” means the page's start or the paragraph's start. The browser's answer is simpler: it means the styled element's start.
A short implementation checklist#
- Decide whether each asymmetric edge is physical, block-relative or inline-relative.
- Use
border-inline-startandborder-inline-endfor reading-flow edges. - Read logical radius names as block side first, inline side second.
- Keep
border-radiuswhen all corners match or the shape is intentionally physical. - Put direction-sensitive decoration on the box whose direction gives it meaning.
- Keep DOM order aligned with visual inline order in joined controls.
- Do not mix physical and logical declarations on the same mapped edge without checking cascade order.
- Inspect later
borderandborder-radiusshorthands in component states and responsive rules. - Treat shadows and pseudo-elements as separate mechanisms, even when they resemble borders.
- Compare the same component in LTR, RTL and any nested opposite-direction state it supports.
The central rule is pleasantly small: name the edge by what it does. A start accent should be coded as start; a fixed left divider should remain left; a symmetric corner needs no RTL work at all. Once the stylesheet preserves those meanings, the browser can perform the mapping instead of waiting for another override.
A free Ritla scan can surface direction-sensitive physical properties without an RTL override R030 and directional shadow offsets without an RTL counterpart R032. Use the component tests above to decide what each edge was meant to do, then continue with what should be mirrored in RTL when the design intent itself is unclear. Borders are easy once the edge has a job description.