diff --git a/demo/e2e/rtl.spec.ts b/demo/e2e/rtl.spec.ts
new file mode 100644
index 00000000..7c574bce
--- /dev/null
+++ b/demo/e2e/rtl.spec.ts
@@ -0,0 +1,75 @@
+import { expect, test } from '@playwright/test';
+
+for (const direction of ['ltr', 'rtl'] as const) {
+ test(`${direction} mirrors logical spacing with asymmetric safe areas`, async ({ page }) => {
+ await page.goto('/main/index', { waitUntil: 'networkidle' });
+ await page.evaluate((dir) => {
+ const fixture = document.createElement('div');
+ fixture.id = 'rtl-probe';
+ fixture.dir = dir;
+ fixture.style.cssText =
+ 'position:fixed;inset:0;z-index:99999;--ion-safe-area-left:20px;--ion-safe-area-right:8px;--ios-theme-menu-width:0px';
+
+ const fab = document.createElement('ion-fab');
+ fab.id = 'fab-probe';
+ fab.mode = 'ios';
+ fab.className = 'fab-horizontal-start';
+ fab.innerHTML = '+';
+
+ const tabs = document.createElement('ion-tab-bar');
+ tabs.id = 'tabs-probe';
+ tabs.mode = 'ios';
+ tabs.slot = 'bottom';
+ tabs.className = 'tab-bar-position-start';
+ tabs.innerHTML = 'OneTwo';
+
+ const list = document.createElement('ion-list');
+ list.id = 'list-probe';
+ list.mode = 'ios';
+ list.className = 'list-inset';
+ list.innerHTML =
+ 'Supporting text
ItemFooter';
+
+ const toolbar = document.createElement('ion-toolbar');
+ toolbar.id = 'toolbar-probe';
+ toolbar.mode = 'ios';
+ toolbar.className = 'toolbar-searchbar';
+ toolbar.innerHTML = 'Action';
+
+ fixture.append(fab, tabs, list, toolbar);
+ document.body.append(fixture);
+ }, direction);
+
+ await expect(page.locator('#fab-probe')).toHaveClass(/hydrated/);
+ await expect(page.locator('#tabs-probe')).toHaveClass(/hydrated/);
+ await expect(page.locator('#list-probe')).toHaveClass(/hydrated/);
+
+ const values = await page.locator('#rtl-probe').evaluate((fixture) => {
+ const direction = fixture.getAttribute('dir');
+ const style = (selector: string) => getComputedStyle(fixture.querySelector(selector)!);
+ const fab = fixture.querySelector('#fab-probe')!.getBoundingClientRect();
+ const tabs = fixture.querySelector('#tabs-probe')!.getBoundingClientRect();
+ const note = style('#list-probe > ion-note');
+ const radio = style('.radio-group-top');
+ const toolbarButtons = style('#toolbar-probe ion-buttons');
+
+ return {
+ fabStart: direction === 'ltr' ? fab.left : innerWidth - fab.right,
+ tabsStart: direction === 'ltr' ? tabs.left : innerWidth - tabs.right,
+ noteStart: note.marginInlineStart,
+ noteEnd: note.marginInlineEnd,
+ radioStart: radio.paddingInlineStart,
+ radioEnd: radio.paddingInlineEnd,
+ toolbarGap: toolbarButtons.marginInlineEnd,
+ };
+ });
+
+ expect(values.fabStart).toBeCloseTo(16, 1);
+ expect(values.tabsStart).toBeCloseTo(direction === 'ltr' ? 36 : 24, 1);
+ expect(values.noteStart).toBe(direction === 'ltr' ? '40px' : '28px');
+ expect(values.noteEnd).toBe(direction === 'ltr' ? '28px' : '40px');
+ expect(values.radioStart).toBe(direction === 'ltr' ? '40px' : '28px');
+ expect(values.radioEnd).toBe(direction === 'ltr' ? '28px' : '40px');
+ expect(values.toolbarGap).toBe('6px');
+ });
+}
diff --git a/demo/src/app/album/album-page.component.scss b/demo/src/app/album/album-page.component.scss
index 3fb8f93c..238d15d7 100644
--- a/demo/src/app/album/album-page.component.scss
+++ b/demo/src/app/album/album-page.component.scss
@@ -1,7 +1,7 @@
ion-header {
position: absolute;
top: 0;
- left: 0;
+ inset-inline-start: 0;
}
ion-content {
diff --git a/demo/src/app/index/pages/button/button.page.scss b/demo/src/app/index/pages/button/button.page.scss
index 27efdf19..fcea56bf 100644
--- a/demo/src/app/index/pages/button/button.page.scss
+++ b/demo/src/app/index/pages/button/button.page.scss
@@ -1,3 +1,3 @@
ion-item *[slot='end'] {
- margin-right: 6px;
+ margin-inline-end: 6px;
}
diff --git a/demo/src/app/index/pages/tabs/tabs.page.scss b/demo/src/app/index/pages/tabs/tabs.page.scss
index 1fa678c4..0676826b 100644
--- a/demo/src/app/index/pages/tabs/tabs.page.scss
+++ b/demo/src/app/index/pages/tabs/tabs.page.scss
@@ -1,6 +1,9 @@
:host {
ion-tab-bar {
- margin-left: calc(16px + var(--ion-safe-area-left, 0px));
- margin-right: calc(16px + var(--ion-safe-area-right, 0px));
+ margin-inline: calc(16px + var(--ion-safe-area-left, 0px)) calc(16px + var(--ion-safe-area-right, 0px));
+
+ &:dir(rtl) {
+ margin-inline: calc(16px + var(--ion-safe-area-right, 0px)) calc(16px + var(--ion-safe-area-left, 0px));
+ }
}
}
diff --git a/demo/src/app/settings/settings-page.component.scss b/demo/src/app/settings/settings-page.component.scss
index bad2a8ca..b2989ad9 100644
--- a/demo/src/app/settings/settings-page.component.scss
+++ b/demo/src/app/settings/settings-page.component.scss
@@ -14,7 +14,7 @@ ion-item > ion-icon[slot='start'] {
ion-item > ion-icon[slot='start'] {
padding: 4px;
font-size: 1.3rem;
- margin-right: 14px;
+ margin-inline-end: 14px;
border-radius: 8px;
}
}
@@ -24,7 +24,7 @@ ion-item > ion-icon[slot='start'] {
padding: 8px;
width: calc(40px - 16px);
height: calc(40px - 16px);
- margin-right: 16px;
+ margin-inline-end: 16px;
border-radius: 50%;
background: var(--ion-color-light-shade);
}
diff --git a/package-lock.json b/package-lock.json
index 084a1ee8..343fde90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
- "@rdlabo/ionic-theme-utils": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#54c43a91d498a2364215d91f74f678fb696ab00c"
+ "@rdlabo/ionic-theme-utils": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#66eb2fccca10646ebfccdfb3b6c2f3b4c9b00bf6"
},
"devDependencies": {
"@capacitor/core": "^8.5.2",
@@ -2221,7 +2221,8 @@
},
"node_modules/@rdlabo/ionic-theme-utils": {
"version": "0.1.0",
- "resolved": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#54c43a91d498a2364215d91f74f678fb696ab00c",
+ "resolved": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#66eb2fccca10646ebfccdfb3b6c2f3b4c9b00bf6",
+ "integrity": "sha512-0/aRkysY012ByvBJFaKBNiU5BFeR7uHCxuQ/BBprq1eHjOgS2AoyZwcBqhqP3SZDDDhnGKLK/SfLQYRySeFAng==",
"license": "MIT",
"peerDependencies": {
"@ionic/core": ">=8.8.1 <10"
diff --git a/package.json b/package.json
index 073f24fc..1cf1526a 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
},
"homepage": "https://docs.rdlabo.dev/projects/ionic-theme-ios27",
"dependencies": {
- "@rdlabo/ionic-theme-utils": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#54c43a91d498a2364215d91f74f678fb696ab00c"
+ "@rdlabo/ionic-theme-utils": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-utils.git#66eb2fccca10646ebfccdfb3b6c2f3b4c9b00bf6"
},
"devDependencies": {
"@capacitor/core": "^8.5.2",
diff --git a/src/styles/components/ion-button.scss b/src/styles/components/ion-button.scss
index 34dc57f9..5a0cacfb 100644
--- a/src/styles/components/ion-button.scss
+++ b/src/styles/components/ion-button.scss
@@ -416,12 +416,13 @@ ion-buttons.ios:not(.ios-theme-disabled, .ios26-disabled):not(
ion-buttons.ios:not(.ios-theme-disabled, .ios26-disabled):not(:has(ion-back-button, ion-button.button-clear)) {
ion-button {
- margin: 0 6px;
+ margin-block: 0;
+ margin-inline: 6px;
&:first-child {
- margin-left: 0;
+ margin-inline-start: 0;
}
&:last-child {
- margin-right: 0;
+ margin-inline-end: 0;
}
&:only-child {
margin: 0;
diff --git a/src/styles/components/ion-fab.scss b/src/styles/components/ion-fab.scss
index 5ea88935..631318a9 100644
--- a/src/styles/components/ion-fab.scss
+++ b/src/styles/components/ion-fab.scss
@@ -6,7 +6,13 @@ $scaleup-default-icon-only: api.$button-scaleup-default-icon-only;
$scaleup-large-icon-only: api.$button-scaleup-large-icon-only;
ion-fab.ios:not(.ios-theme-disabled, .ios26-disabled) {
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-left, 0px);
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-right, 0px);
--transform-value: 2.5px;
+ &:dir(rtl) {
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-right, 0px);
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-left, 0px);
+ }
&:has(> ion-fab-button.fab-button-small) {
--transform-value: 4px;
}
@@ -85,20 +91,20 @@ ion-fab.ios:not(.ios-theme-disabled, .ios26-disabled) {
}
&.fab-horizontal-start {
- left: calc(16px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ion-safe-area-left), 0px)));
+ inset-inline-start: calc(16px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ios-theme-safe-area-inline-start))));
}
&.fab-horizontal-end {
- right: calc(16px + var(--ion-safe-area-right, 0px));
+ inset-inline-end: calc(16px + var(--ios-theme-safe-area-inline-end));
}
&:has(.fab-button-small) {
&.fab-horizontal-start {
- left: calc(10px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ion-safe-area-left), 0px)));
+ inset-inline-start: calc(10px + var(--ios-theme-menu-width, var(--ios26-menu-width, var(--ios-theme-safe-area-inline-start))));
}
&.fab-horizontal-end {
- right: calc(10px + var(--ion-safe-area-right, 0px));
+ inset-inline-end: calc(10px + var(--ios-theme-safe-area-inline-end));
}
}
}
diff --git a/src/styles/components/ion-list.scss b/src/styles/components/ion-list.scss
index 2b6117bd..f1b55d78 100644
--- a/src/styles/components/ion-list.scss
+++ b/src/styles/components/ion-list.scss
@@ -23,13 +23,22 @@ ion-list.list-inset.ios:not(.ios-theme-disabled, .ios26-disabled) {
padding-inline-end: calc(var(--ion-safe-area-right, 0) + 20px);
}
+ &:dir(rtl) ion-radio-group > .radio-group-top {
+ padding-inline-start: calc(var(--ion-safe-area-right, 0) + 20px);
+ padding-inline-end: calc(var(--ion-safe-area-left, 0) + 20px);
+ }
+
> :is(#{structured-list.$groups}) {
ion-item {
// To draw lines inside
--inner-padding-end: 0;
&::part(native) {
- padding-right: calc(var(--ion-safe-area-right, 0px) + 18px);
+ padding-inline-end: calc(var(--ion-safe-area-right, 0px) + 18px);
+ }
+
+ &:dir(rtl)::part(native) {
+ padding-inline-end: calc(var(--ion-safe-area-left, 0px) + 18px);
}
--min-height: 52px;
diff --git a/src/styles/components/ion-menu.scss b/src/styles/components/ion-menu.scss
index 9c1da635..3d437eb9 100644
--- a/src/styles/components/ion-menu.scss
+++ b/src/styles/components/ion-menu.scss
@@ -6,13 +6,13 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
.split-pane-main {
ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
width: calc(
- 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-left, 0px)) - 60px -
+ 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-right, 0px)) - 60px -
12px - var(--ios-theme-menu-width, var(--ios26-menu-width))
);
&:has(:nth-child(5)) {
width: calc(
- 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-left, 0px)) - var(
+ 100% - calc(18px + var(--ion-safe-area-left, 0px)) - calc(18px + var(--ion-safe-area-right, 0px)) - var(
--ios-theme-menu-width,
var(--ios26-menu-width)
)
@@ -31,7 +31,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
/*
* 90px: https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/title/title.ios.scss#L8
*/
- padding-left: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
+ padding-inline-start: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
}
}
ion-content {
@@ -45,7 +45,10 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
}
ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
&[slot='bottom'] {
- left: calc(16px + var(--ion-safe-area-left, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
+ inset-inline-start: calc(16px + var(--ion-safe-area-left, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
+ &:dir(rtl) {
+ inset-inline-start: calc(16px + var(--ion-safe-area-right, 0px) + var(--ios-theme-menu-width, var(--ios26-menu-width)));
+ }
}
}
}
@@ -62,7 +65,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
/*
* 90px: https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/title/title.ios.scss#L8
*/
- padding-right: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
+ padding-inline-end: calc(90px + var(--ios-theme-menu-width, var(--ios26-menu-width)));
}
}
ion-content {
@@ -88,10 +91,10 @@ ion-menu.ios:not(.ios-theme-disabled, .ios26-disabled) {
z-index: 999;
&.menu-side-start {
- left: 0;
+ inset-inline-start: 0;
}
&.menu-side-end {
- left: calc(100vw - var(--ios-theme-menu-width, var(--ios26-menu-width)));
+ inset-inline-end: 0;
}
&::part(backdrop) {
@@ -119,9 +122,13 @@ ion-menu.ios:not(.ios-theme-disabled, .ios26-disabled) {
--ios-theme-content-box-shadow-rgb: var(--ios26-content-box-shadow-rgb);
&::part(scroll) {
- margin: calc(var(--ios-theme-floating-safe-area-top, var(--ios26-floating-safe-area-top)) + 6px)
- calc(16px + var(--ion-safe-area-right, 0px)) var(--ios-theme-floating-safe-area-bottom, var(--ios26-floating-safe-area-bottom))
- calc(12px + var(--ion-safe-area-left, 0px));
+ margin-block: calc(var(--ios-theme-floating-safe-area-top, var(--ios26-floating-safe-area-top)) + 6px)
+ var(--ios-theme-floating-safe-area-bottom, var(--ios26-floating-safe-area-bottom));
+ margin-inline: calc(12px + var(--ion-safe-area-left, 0px)) calc(16px + var(--ion-safe-area-right, 0px));
+ }
+
+ &:dir(rtl)::part(scroll) {
+ margin-inline: calc(12px + var(--ion-safe-area-right, 0px)) calc(16px + var(--ion-safe-area-left, 0px));
}
}
}
diff --git a/src/styles/components/ion-searchbar.scss b/src/styles/components/ion-searchbar.scss
index 5e3cec43..b279343c 100644
--- a/src/styles/components/ion-searchbar.scss
+++ b/src/styles/components/ion-searchbar.scss
@@ -274,6 +274,6 @@ ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled).searchbar-classic {
width: 1.18rem;
}
button.searchbar-clear-button {
- right: 8px;
+ inset-inline-end: 8px;
}
}
diff --git a/src/styles/components/ion-tabs.scss b/src/styles/components/ion-tabs.scss
index bd8b0335..61622ecf 100644
--- a/src/styles/components/ion-tabs.scss
+++ b/src/styles/components/ion-tabs.scss
@@ -1,6 +1,12 @@
@use '../utils/api';
ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-left, 0px);
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-right, 0px);
+ &:dir(rtl) {
+ --ios-theme-safe-area-inline-start: var(--ion-safe-area-right, 0px);
+ --ios-theme-safe-area-inline-end: var(--ion-safe-area-left, 0px);
+ }
// Ionic forces content-box on the host. Keep the Glass border in a
// separate layer so its physical-pixel rounding cannot resize the bar.
border: 0;
@@ -29,26 +35,24 @@ ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled) {
&[slot='top'] {
position: absolute;
top: var(--ios-theme-floating-safe-area-top, var(--ios26-floating-safe-area-top));
- right: calc(16px + var(--ion-safe-area-right, 0px));
+ inset-inline-end: calc(16px + var(--ios-theme-safe-area-inline-end));
}
&[slot='bottom'] {
position: absolute;
bottom: var(--ios-theme-floating-safe-area-bottom, var(--ios26-floating-safe-area-bottom));
- left: calc(16px + var(--ion-safe-area-left, 0px));
+ inset-inline-start: calc(16px + var(--ios-theme-safe-area-inline-start));
}
- &.tab-bar-position-start,
- &.tab-bar-position-end:dir(rtl) {
- left: calc(16px + var(--ion-safe-area-left, 0px));
- right: auto;
+ &.tab-bar-position-start {
+ inset-inline-start: calc(16px + var(--ios-theme-safe-area-inline-start));
+ inset-inline-end: auto;
margin-inline: 0;
}
- &.tab-bar-position-end,
- &.tab-bar-position-start:dir(rtl) {
- left: auto;
- right: calc(16px + var(--ion-safe-area-right, 0px));
+ &.tab-bar-position-end {
+ inset-inline-start: auto;
+ inset-inline-end: calc(16px + var(--ios-theme-safe-area-inline-end));
margin-inline: 0;
}
diff --git a/src/styles/components/ion-toolbar.scss b/src/styles/components/ion-toolbar.scss
index 14071ee1..51564e79 100644
--- a/src/styles/components/ion-toolbar.scss
+++ b/src/styles/components/ion-toolbar.scss
@@ -57,27 +57,29 @@ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled).toolbar-searchbar {
}
&:has(ion-buttons[slot='start']) {
ion-buttons[slot='start'] {
- margin-right: 6px;
+ margin-inline-end: 6px;
}
ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
&:not(.searchbar-classic) {
padding-inline: 6px 12px;
}
&.searchbar-classic .searchbar-input-container {
- margin: 0 12px 0 6px;
+ margin-block: 0;
+ margin-inline: 6px 12px;
}
}
}
&:has(ion-buttons[slot='end']) {
ion-buttons[slot='end'] {
- margin-left: 6px;
+ margin-inline-start: 6px;
}
ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
&:not(.searchbar-classic) {
padding-inline: 12px 6px;
}
&.searchbar-classic .searchbar-input-container {
- margin: 0 6px 0 12px;
+ margin-block: 0;
+ margin-inline: 12px 6px;
}
}
}
@@ -85,15 +87,12 @@ ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled).toolbar-searchbar {
ion-toolbar.ios:not(.ios-theme-disabled, .ios26-disabled) {
ion-buttons {
- margin-left: 12px;
- margin-right: 12px;
+ margin-inline: 12px;
}
ion-back-button {
- margin-left: 12px;
- margin-right: 12px;
+ margin-inline: 12px;
}
ion-buttons > ion-back-button {
- margin-left: 0;
- margin-right: 0;
+ margin-inline: 0;
}
}