@rdlabo/ionic-theme-ios27 1.2.0-1 → 1.2.0-beta.pr226.shaa2e125f11160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -17,24 +17,55 @@ export const registerTabBarEffect = (targetElement: HTMLElement): registeredEffe
17
17
  const reducedMotion = win.matchMedia('(prefers-reduced-motion: reduce)');
18
18
  const verticalBarsRoot = targetElement.closest('ion-app');
19
19
  let effect: registeredEffect | undefined;
20
+ let placement: 'disabled' | 'horizontal' | 'left' | 'right' | undefined;
20
21
  const update = () => {
21
- effect?.destroy();
22
22
  const isVertical =
23
- !!verticalBarsRoot?.classList.contains('ios-theme-vertical-bars') && !targetElement.closest('ion-menu, ion-modal, ion-popover');
24
- effect =
25
- reducedMotion.matches || isVertical
26
- ? undefined
27
- : registerEffect(targetElement, 'ion-tab-button', 'tab-selected', {
23
+ !!verticalBarsRoot?.classList.contains('ios-theme-vertical-bars') &&
24
+ targetElement.matches('ion-tabs > ion-tab-bar:not(.ios-theme-disabled, .ios26-disabled)') &&
25
+ !targetElement.closest('ion-menu, ion-modal, ion-popover');
26
+ const disabled = reducedMotion.matches || !targetElement.matches('.ios:not(.ios-theme-disabled, .ios26-disabled)');
27
+ const nextPlacement = disabled
28
+ ? 'disabled'
29
+ : isVertical
30
+ ? verticalBarsRoot?.classList.contains('ios-theme-vertical-bars-left')
31
+ ? 'left'
32
+ : 'right'
33
+ : 'horizontal';
34
+ // Gesture/activation classes also change during interaction. Rebuild only when
35
+ // eligibility or placement changes, retaining cancellation when the rail changes sides.
36
+ if (placement === nextPlacement) return;
37
+ placement = nextPlacement;
38
+ effect?.destroy();
39
+ effect = disabled
40
+ ? undefined
41
+ : registerEffect(
42
+ targetElement,
43
+ 'ion-tab-button',
44
+ 'tab-selected',
45
+ {
28
46
  small: 'scale(1.1, 1)',
29
47
  medium: 'scale(1.2)',
30
48
  large: 'scale(1.15, 1.4)',
31
49
  xlarge: 'scale(1.3)',
32
- });
50
+ },
51
+ isVertical,
52
+ );
33
53
  };
34
54
  update();
35
55
  reducedMotion.addEventListener('change', update);
36
- const placementObserver = new win.MutationObserver(update);
37
- if (verticalBarsRoot) placementObserver.observe(verticalBarsRoot, { attributes: true, attributeFilter: ['class'] });
56
+ const placementObserver = new win.MutationObserver((records) => {
57
+ if (
58
+ records.some((record) =>
59
+ record.type === 'attributes'
60
+ ? record.target === targetElement || record.target === verticalBarsRoot
61
+ : [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some((node) => node.contains(targetElement)),
62
+ )
63
+ )
64
+ update();
65
+ });
66
+ if (verticalBarsRoot)
67
+ placementObserver.observe(verticalBarsRoot, { attributes: true, attributeFilter: ['class'], childList: true, subtree: true });
68
+ placementObserver.observe(targetElement, { attributes: true, attributeFilter: ['class'] });
38
69
  return {
39
70
  destroy: () => {
40
71
  reducedMotion.removeEventListener('change', update);
@@ -3,6 +3,21 @@ import { AnimationPosition, EffectScales } from './interfaces';
3
3
  import { createAnimation, GestureDetail } from '@ionic/core';
4
4
  import { getStep } from '../utils';
5
5
 
6
+ // Work in main/cross-axis coordinates so native horizontal samples also drive the rail.
7
+ export const animationRect = (element: Element, vertical = false) => {
8
+ const rect = element.getBoundingClientRect();
9
+ return vertical
10
+ ? { left: rect.top, top: rect.left, right: rect.bottom, width: rect.height, height: rect.width, x: rect.y, y: rect.x }
11
+ : rect;
12
+ };
13
+ const translate = (x: number, y: number, vertical: boolean) => `translate3d(${vertical ? y : x}px, ${vertical ? x : y}px, 0)`;
14
+ const stretchTransform = (x: number, y: number, vertical: boolean) => `scale(${vertical ? y : x}, ${vertical ? x : y})`;
15
+ const mainSize = (element: HTMLElement, vertical: boolean) => (vertical ? element.offsetHeight : element.offsetWidth);
16
+ const axisScale = (transform: string, vertical: boolean) => {
17
+ const matrix = new DOMMatrixReadOnly(transform);
18
+ return vertical ? { a: matrix.d, d: matrix.a } : matrix;
19
+ };
20
+
6
21
  export const getScaleAnimation = (effectElement: Element): Animation => {
7
22
  return createAnimation().addElement(effectElement.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!).easing('ease-out');
8
23
  };
@@ -46,21 +61,27 @@ const tabBarTapFrames = [
46
61
  ];
47
62
 
48
63
  // UITabBarController presentation-layer samples: horizontal stretch precedes vertical stretch.
49
- export const createTabBarPressAnimation = (effect: HTMLElement, from: HTMLElement, to: HTMLElement, bar: HTMLElement): Animation => {
50
- const box = bar.getBoundingClientRect();
64
+ export const createTabBarPressAnimation = (
65
+ effect: HTMLElement,
66
+ from: HTMLElement,
67
+ to: HTMLElement,
68
+ bar: HTMLElement,
69
+ vertical = false,
70
+ ): Animation => {
71
+ const box = animationRect(bar, vertical);
51
72
  const center = box.left + box.width / 2;
52
- const left = center - bar.offsetWidth / 2;
53
- const fromBox = from.getBoundingClientRect();
54
- const toBox = to.getBoundingClientRect();
55
- const parentScale = box.width / bar.offsetWidth;
56
- const buttonScale = new DOMMatrixReadOnly(getComputedStyle(from).transform);
73
+ const left = center - mainSize(bar, vertical) / 2;
74
+ const fromBox = animationRect(from, vertical);
75
+ const toBox = animationRect(to, vertical);
76
+ const parentScale = box.width / mainSize(bar, vertical);
77
+ const buttonScale = axisScale(getComputedStyle(from).transform, vertical);
57
78
  const width = fromBox.width / parentScale / buttonScale.a;
58
79
  const height = fromBox.height / parentScale / buttonScale.d;
59
80
  const start = (fromBox.left + fromBox.width / 2 - box.left) / parentScale;
60
81
  const end = (toBox.left + toBox.width / 2 - box.left) / parentScale;
61
82
  const selected = from === to;
62
83
  const frames = tabBarPressFrames.map(([time, position, extraWidth, extraHeight, expansion]) => {
63
- const scale = 1 + (14.14 / bar.offsetWidth) * expansion;
84
+ const scale = 1 + (14.14 / mainSize(bar, vertical)) * expansion;
64
85
  // A selected item expands in place, without the transfer's sideways deformation.
65
86
  const press = 1 - Math.exp(-18 * time) * (1 + 18 * time);
66
87
  return {
@@ -73,26 +94,24 @@ export const createTabBarPressAnimation = (effect: HTMLElement, from: HTMLElemen
73
94
  const move = createAnimation()
74
95
  .addElement(effect)
75
96
  .beforeStyles({
76
- width: `${width}px`,
77
- height: `${height}px`,
97
+ width: `${vertical ? height : width}px`,
98
+ height: `${vertical ? width : height}px`,
78
99
  display: 'block',
79
100
  opacity: '1',
80
101
  })
81
- .keyframes(
82
- frames.map(({ offset, x }) => ({ offset, transform: `translate3d(${x}px, ${box.top + box.height / 2 - height / 2}px, 0)` })),
83
- );
102
+ .keyframes(frames.map(({ offset, x }) => ({ offset, transform: translate(x, box.top + box.height / 2 - height / 2, vertical) })));
84
103
  const stretch = getScaleAnimation(effect)
85
104
  .easing('linear')
86
- .keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform: `scale(${sx}, ${sy})` })));
105
+ .keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform: stretchTransform(sx, sy, vertical) })));
87
106
  return createAnimation().duration(900).easing('linear').addAnimation([move, stretch]);
88
107
  };
89
108
 
90
109
  // Drag samples: faster movement stretches sideways, then rebounds vertically at rest.
91
- export const createTabBarDragAnimation = (effect: HTMLElement, bar: HTMLElement, velocity: number): Animation => {
110
+ export const createTabBarDragAnimation = (effect: HTMLElement, bar: HTMLElement, velocity: number, vertical = false): Animation => {
92
111
  const native = effect.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!;
93
- const current = new DOMMatrixReadOnly(native.style.transform || getComputedStyle(native).transform);
94
- const { width, height } = effect.getBoundingClientRect();
95
- const scale = 1 + 14.14 / bar.offsetWidth;
112
+ const current = axisScale(native.style.transform || getComputedStyle(native).transform, vertical);
113
+ const { width, height } = animationRect(effect, vertical);
114
+ const scale = 1 + 14.14 / mainSize(bar, vertical);
96
115
  const restX = ((width + 16) * scale) / width;
97
116
  const restY = ((height + 16) * scale) / height;
98
117
  // Approximate the 100/600pt/s UIKit samples; keep deformation bounded for fast swipes.
@@ -102,10 +121,10 @@ export const createTabBarDragAnimation = (effect: HTMLElement, bar: HTMLElement,
102
121
  .duration(500)
103
122
  .easing('linear')
104
123
  .keyframes([
105
- { offset: 0, transform: `scale(${current.a}, ${current.d})` },
106
- { offset: 0.2, transform: `scale(${restX + stretch / width}, ${restY - stretch / height})` },
107
- { offset: 0.44, transform: `scale(${restX - rebound / width}, ${restY + rebound / height})` },
108
- { offset: 1, transform: `scale(${restX}, ${restY})` },
124
+ { offset: 0, transform: stretchTransform(current.a, current.d, vertical) },
125
+ { offset: 0.2, transform: stretchTransform(restX + stretch / width, restY - stretch / height, vertical) },
126
+ { offset: 0.44, transform: stretchTransform(restX - rebound / width, restY + rebound / height, vertical) },
127
+ { offset: 1, transform: stretchTransform(restX, restY, vertical) },
109
128
  ]);
110
129
  };
111
130
 
@@ -114,10 +133,11 @@ export const createTabBarReleaseAnimation = (
114
133
  effectElement: Element,
115
134
  target: HTMLElement,
116
135
  tap?: { elapsed: number; distance: number },
136
+ vertical = false,
117
137
  ): Animation => {
118
138
  const tapElapsed = tap?.elapsed;
119
139
  const native = effectElement.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!;
120
- const scale = new DOMMatrixReadOnly(native.style.transform || getComputedStyle(native).transform);
140
+ const scale = axisScale(native.style.transform || getComputedStyle(native).transform, vertical);
121
141
  const samples = [
122
142
  [0, 1],
123
143
  [0.033, 0.85],
@@ -131,11 +151,11 @@ export const createTabBarReleaseAnimation = (
131
151
  [0.45, 0],
132
152
  ];
133
153
  const bar = target.parentElement!;
134
- const box = bar.getBoundingClientRect();
135
- const current = effectElement.getBoundingClientRect();
136
- const targetBox = target.getBoundingClientRect();
154
+ const box = animationRect(bar, vertical);
155
+ const current = animationRect(effectElement, vertical);
156
+ const targetBox = animationRect(target, vertical);
137
157
  const center = box.left + box.width / 2;
138
- const x = center + (targetBox.left + targetBox.width / 2 - center) / (box.width / bar.offsetWidth) - current.width / 2;
158
+ const x = center + (targetBox.left + targetBox.width / 2 - center) / (box.width / mainSize(bar, vertical)) - current.width / 2;
139
159
  const y = box.top + box.height / 2 - current.height / 2;
140
160
  const elapsed = (tapElapsed ?? 0) / 1000;
141
161
  const duration = tapElapsed === undefined ? 450 : 800 - tapElapsed;
@@ -159,7 +179,7 @@ export const createTabBarReleaseAnimation = (
159
179
  ...tabBarTapFrames.slice(nextIndex).map(([time, position, width, height]) => ({
160
180
  offset: (time - elapsed) / (0.8 - elapsed),
161
181
  x: x + ((current.x - x) * (1 - position)) / Math.max(0.1, 1 - progress),
162
- y,
182
+ y: y + ((current.y - y) * (1 - position)) / Math.max(0.1, 1 - progress),
163
183
  sx: 1 + (width * (time >= 0.367 ? 1 + 0.75 * extraDistance : 1)) / current.width,
164
184
  sy: 1 + (height * (time >= 0.367 ? 1 + 0.5 * extraDistance : 1)) / current.height,
165
185
  })),
@@ -169,7 +189,7 @@ export const createTabBarReleaseAnimation = (
169
189
  .addElement(native)
170
190
  .duration(duration)
171
191
  .easing('linear')
172
- .keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform: `scale(${sx}, ${sy})` })));
192
+ .keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform: stretchTransform(sx, sy, vertical) })));
173
193
  const move = createAnimation()
174
194
  .addElement(effectElement)
175
195
  .duration(duration)
@@ -177,7 +197,7 @@ export const createTabBarReleaseAnimation = (
177
197
  .keyframes(
178
198
  frames.map(({ offset, x, y }) => ({
179
199
  offset,
180
- transform: `translate3d(${x}px, ${y}px, 0)`,
200
+ transform: translate(x, y, vertical),
181
201
  })),
182
202
  );
183
203
  // Blend the reflective lens into the selected surface during the final 200ms.
@@ -204,7 +224,7 @@ export const createTabBarReleaseAnimation = (
204
224
  if (tapElapsed !== undefined) {
205
225
  // A short UIKit tap keeps expanding after lift-off, then settles independently of the lens.
206
226
  // Width deltas from the 50ms tap; preserve the rendered size for an uninterrupted handoff.
207
- const width = bar.offsetWidth;
227
+ const width = mainSize(bar, vertical);
208
228
  const expansion = box.width - width;
209
229
  const peak = Math.max(8.65, expansion);
210
230
  const platter = createAnimation()
@@ -282,6 +302,7 @@ export const createMoveAnimation = (
282
302
  detail: GestureDetail,
283
303
  tabSelectedElement: Element,
284
304
  animationPosition: AnimationPosition,
305
+ vertical = false,
285
306
  ): Animation => {
286
307
  return createAnimation()
287
308
  .duration(500)
@@ -295,8 +316,8 @@ export const createMoveAnimation = (
295
316
  })
296
317
  .fromTo(
297
318
  'transform',
298
- `translate3d(${animationPosition.minPositionX}px, ${animationPosition.positionY}px, 0)`,
299
- `translate3d(${animationPosition.maxPositionX}px, ${animationPosition.positionY}px, 0)`,
319
+ translate(animationPosition.minPositionX, animationPosition.positionY, vertical),
320
+ translate(animationPosition.maxPositionX, animationPosition.positionY, vertical),
300
321
  )
301
322
  .progressStep(getStep(detail.currentX, animationPosition));
302
323
  };
@@ -4,6 +4,7 @@ import type { Animation, Gesture, GestureDetail } from '@ionic/core';
4
4
  import { changeSelectedElement, cloneElement, getStep } from '../utils';
5
5
  import { isNativeUIShell } from '../native-integration';
6
6
  import {
7
+ animationRect,
7
8
  createMoveAnimation,
8
9
  createPreMoveAnimation,
9
10
  createTabBarDragAnimation,
@@ -21,6 +22,7 @@ export const registerEffect = (
21
22
  effectTagName: string,
22
23
  selectedClassName: string,
23
24
  scales: EffectScales,
25
+ vertical = false,
24
26
  ): registeredEffect | undefined => {
25
27
  if (!targetElement.classList.contains('ios')) {
26
28
  return undefined;
@@ -42,6 +44,7 @@ export const registerEffect = (
42
44
  let destroyed = false;
43
45
  const originalTabBarScale = targetElement.style.getPropertyValue('--ios27-tab-bar-scale');
44
46
  const effectElement = cloneElement(effectTagName, false);
47
+ if (vertical) effectElement.classList.add('ios27-vertical-tab-effect');
45
48
  const stopTabPress = () => {
46
49
  if (!tabPressAnimation) return;
47
50
  const native = effectElement.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!;
@@ -122,17 +125,30 @@ export const registerEffect = (
122
125
  };
123
126
  targetElement.addEventListener('pointerdown', onPointerDown);
124
127
 
128
+ const axisDetail = (detail: GestureDetail): GestureDetail =>
129
+ vertical
130
+ ? {
131
+ ...detail,
132
+ currentX: detail.currentY,
133
+ currentY: detail.currentX,
134
+ startX: detail.startY,
135
+ startY: detail.startX,
136
+ velocityX: detail.velocityY,
137
+ velocityY: detail.velocityX,
138
+ }
139
+ : detail;
125
140
  const createAnimationGesture = () => {
126
141
  targetElement.classList.add(GESTURE_NAME);
127
142
  gesture = createGesture({
128
143
  el: targetElement,
129
144
  threshold: 0,
145
+ direction: vertical ? 'y' : 'x',
130
146
  gestureName: `${GESTURE_NAME}_${effectTagName}_${crypto.randomUUID()}`,
131
- onStart: (event) => onStartGesture(event),
132
- onMove: (event) => onMoveGesture(event),
147
+ onStart: (event) => onStartGesture(axisDetail(event)),
148
+ onMove: (event) => onMoveGesture(axisDetail(event)),
133
149
  onEnd: (detail) => {
134
150
  // Ionic coalesces moves; consume the release coordinate before committing a drag.
135
- if (effectTagName === 'ion-tab-button' && tabDragging) onMoveGesture(detail);
151
+ if (effectTagName === 'ion-tab-button' && tabDragging) onMoveGesture(axisDetail(detail));
136
152
  onEndGesture().then();
137
153
  },
138
154
  });
@@ -160,34 +176,48 @@ export const registerEffect = (
160
176
  if (isNativeUIShell(targetElement)) return false;
161
177
  currentTouchedElement = ((detail.event.target as HTMLElement).closest(effectTagName) as HTMLElement) || undefined;
162
178
  const tabSelectedElement = targetElement.querySelector(`${effectTagName}.${selectedClassName}`);
163
- if (currentTouchedElement === undefined || tabSelectedElement === null) {
179
+ if (
180
+ currentTouchedElement === undefined ||
181
+ currentTouchedElement.parentElement !== targetElement ||
182
+ (currentTouchedElement as HTMLIonTabButtonElement).disabled ||
183
+ tabSelectedElement === null
184
+ ) {
185
+ currentTouchedElement = undefined;
164
186
  return false;
165
187
  }
166
188
  selectedElementBeforeGesture = tabSelectedElement as HTMLElement;
167
189
  if (effectTagName === 'ion-tab-button') {
168
190
  // UIKit expands the platter by 14.14pt at both two- and four-tab widths.
169
- targetElement.style.setProperty('--ios27-tab-bar-scale', String(1 + 14.14 / targetElement.offsetWidth));
191
+ targetElement.style.setProperty(
192
+ '--ios27-tab-bar-scale',
193
+ String(1 + 14.14 / (vertical ? targetElement.offsetHeight : targetElement.offsetWidth)),
194
+ );
170
195
  }
171
196
  animationPosition = {
172
- minPositionX: targetElement.getBoundingClientRect().left,
173
- maxPositionX: targetElement.getBoundingClientRect().right - tabSelectedElement.clientWidth,
174
- width: tabSelectedElement.clientWidth,
175
- positionY: tabSelectedElement.getBoundingClientRect().top,
197
+ minPositionX: animationRect(targetElement, vertical).left,
198
+ maxPositionX:
199
+ animationRect(targetElement, vertical).right - (vertical ? tabSelectedElement.clientHeight : tabSelectedElement.clientWidth),
200
+ width: vertical ? tabSelectedElement.clientHeight : tabSelectedElement.clientWidth,
201
+ positionY: animationRect(tabSelectedElement, vertical).top,
176
202
  };
177
203
  targetElement.classList.add(ANIMATED_NAME);
178
204
  changeSelectedElement(targetElement, currentTouchedElement, effectTagName, selectedClassName);
179
205
 
180
206
  if (effectTagName === 'ion-tab-button') {
181
- moveAnimation = createMoveAnimation(effectElement, detail, tabSelectedElement, animationPosition);
207
+ moveAnimation = createMoveAnimation(effectElement, detail, tabSelectedElement, animationPosition, vertical);
182
208
  moveAnimation.progressStart(
183
209
  true,
184
- getStep(tabSelectedElement.getBoundingClientRect().left + tabSelectedElement.clientWidth / 2, animationPosition),
210
+ getStep(
211
+ animationRect(tabSelectedElement, vertical).left + animationRect(tabSelectedElement, vertical).width / 2,
212
+ animationPosition,
213
+ ),
185
214
  );
186
215
  tabPressAnimation = createTabBarPressAnimation(
187
216
  effectElement,
188
217
  tabSelectedElement as HTMLElement,
189
218
  currentTouchedElement,
190
219
  targetElement,
220
+ vertical,
191
221
  );
192
222
  tabPressStartedAt = performance.now();
193
223
  void tabPressAnimation.play();
@@ -225,7 +255,7 @@ export const registerEffect = (
225
255
  if (Math.abs(detail.currentX - detail.startX) < 3 && !tabDragging) return;
226
256
  tabDragging = true;
227
257
  stopTabPress();
228
- tabPressAnimation = createTabBarDragAnimation(effectElement, targetElement, detail.velocityX);
258
+ tabPressAnimation = createTabBarDragAnimation(effectElement, targetElement, detail.velocityX, vertical);
229
259
  void tabPressAnimation.play();
230
260
  } else if (scaleAnimationPromise === undefined) {
231
261
  if (Math.abs(detail.velocityX) > maxVelocity) {
@@ -249,11 +279,19 @@ export const registerEffect = (
249
279
  }
250
280
 
251
281
  const currentX = detail.currentX;
252
- const previousY = targetElement.getBoundingClientRect().top + targetElement.getBoundingClientRect().height / 2;
253
- const nextEl = (targetElement.getRootNode() as Document | ShadowRoot).elementFromPoint(currentX, previousY);
282
+ const previousY = animationRect(targetElement, vertical).top + animationRect(targetElement, vertical).height / 2;
283
+ const nextEl = (targetElement.getRootNode() as Document | ShadowRoot).elementFromPoint(
284
+ vertical ? previousY : currentX,
285
+ vertical ? currentX : previousY,
286
+ );
254
287
  const latestTouchedElement = (nextEl?.closest(effectTagName) as HTMLElement) || undefined;
255
288
 
256
- if (latestTouchedElement && currentTouchedElement !== latestTouchedElement) {
289
+ if (
290
+ latestTouchedElement &&
291
+ latestTouchedElement.parentElement === targetElement &&
292
+ !(latestTouchedElement as HTMLIonTabButtonElement).disabled &&
293
+ currentTouchedElement !== latestTouchedElement
294
+ ) {
257
295
  currentTouchedElement = latestTouchedElement;
258
296
  changeSelectedElement(targetElement, currentTouchedElement, effectTagName, selectedClassName);
259
297
  }
@@ -317,6 +355,7 @@ export const registerEffect = (
317
355
  distance: Math.abs(tabs.indexOf(currentTouchedElement) - tabs.indexOf(selectedElementBeforeGesture!)),
318
356
  }
319
357
  : undefined,
358
+ vertical,
320
359
  );
321
360
  currentTouchedElement.classList.remove('ion-activated');
322
361
  releaseAnimation = releasing;
@@ -152,6 +152,10 @@ ion-app.ios-theme-vertical-bars
152
152
  top: auto;
153
153
  bottom: calc(var(--ion-safe-area-bottom, 0px) + var(--ios-theme-side-tab-bar-gap));
154
154
 
155
+ &.ios27-enable-gesture {
156
+ touch-action: pan-x pinch-zoom;
157
+ }
158
+
155
159
  > ion-tab-button:not(.ion-cloned-element) {
156
160
  display: block;
157
161
  flex: none;
@@ -162,6 +166,7 @@ ion-app.ios-theme-vertical-bars
162
166
  border-radius: 23px;
163
167
  &::part(native) {
164
168
  justify-content: center;
169
+ min-height: 52px;
165
170
  }
166
171
  ion-label {
167
172
  position: absolute;
@@ -340,3 +345,12 @@ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-left {
340
345
  left: max(4px, calc((var(--ios-theme-vertical-bars-safe-area-left-resolved) - 46px) / 2));
341
346
  }
342
347
  }
348
+
349
+ // The lens is attached to body, outside the rail selectors.
350
+ ion-tab-button.ios.ion-cloned-element.ios27-vertical-tab-effect {
351
+ border-radius: 23px;
352
+ &::part(native) {
353
+ min-height: 0;
354
+ height: 100%;
355
+ }
356
+ }