@@ -276,7 +276,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
276276
277277 @Listen ( 'resize' , { target : 'window' } )
278278 onWindowResize ( ) {
279- // Only handle resize for iOS card modals when no custom animations are provided
279+ // Update safe-area overrides for all modal types on resize
280+ this . updateSafeAreaOverrides ( ) ;
281+
282+ // Only handle view transition for iOS card modals when no custom animations are provided
280283 if ( getIonMode ( this ) !== 'ios' || ! this . presentingElement || this . enterAnimation || this . leaveAnimation ) {
281284 return ;
282285 }
@@ -659,6 +662,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
659662 this . initSwipeToClose ( ) ;
660663 }
661664
665+ // Set initial safe-area overrides based on modal position
666+ this . updateSafeAreaOverrides ( ) ;
667+
662668 // Initialize view transition listener for iOS card modals
663669 this . initViewTransitionListener ( ) ;
664670
@@ -692,33 +698,39 @@ export class Modal implements ComponentInterface, OverlayInterface {
692698
693699 const statusBarStyle = this . statusBarStyle ?? StatusBarStyle . Default ;
694700
695- this . gesture = createSwipeToCloseGesture ( el , ani , statusBarStyle , ( ) => {
696- /**
697- * While the gesture animation is finishing
698- * it is possible for a user to tap the backdrop.
699- * This would result in the dismiss animation
700- * being played again. Typically this is avoided
701- * by setting `presented = false` on the overlay
702- * component; however, we cannot do that here as
703- * that would prevent the element from being
704- * removed from the DOM.
705- */
706- this . gestureAnimationDismissing = true ;
707-
708- /**
709- * Reset the status bar style as the dismiss animation
710- * starts otherwise the status bar will be the wrong
711- * color for the duration of the dismiss animation.
712- * The dismiss method does this as well, but
713- * in this case it's only called once the animation
714- * has finished.
715- */
716- setCardStatusBarDefault ( this . statusBarStyle ) ;
717- this . animation ! . onFinish ( async ( ) => {
718- await this . dismiss ( undefined , GESTURE ) ;
719- this . gestureAnimationDismissing = false ;
720- } ) ;
721- } ) ;
701+ this . gesture = createSwipeToCloseGesture (
702+ el ,
703+ ani ,
704+ statusBarStyle ,
705+ ( ) => {
706+ /**
707+ * While the gesture animation is finishing
708+ * it is possible for a user to tap the backdrop.
709+ * This would result in the dismiss animation
710+ * being played again. Typically this is avoided
711+ * by setting `presented = false` on the overlay
712+ * component; however, we cannot do that here as
713+ * that would prevent the element from being
714+ * removed from the DOM.
715+ */
716+ this . gestureAnimationDismissing = true ;
717+
718+ /**
719+ * Reset the status bar style as the dismiss animation
720+ * starts otherwise the status bar will be the wrong
721+ * color for the duration of the dismiss animation.
722+ * The dismiss method does this as well, but
723+ * in this case it's only called once the animation
724+ * has finished.
725+ */
726+ setCardStatusBarDefault ( this . statusBarStyle ) ;
727+ this . animation ! . onFinish ( async ( ) => {
728+ await this . dismiss ( undefined , GESTURE ) ;
729+ this . gestureAnimationDismissing = false ;
730+ } ) ;
731+ } ,
732+ ( ) => this . updateSafeAreaOverrides ( )
733+ ) ;
722734 this . gesture . enable ( true ) ;
723735 }
724736
@@ -755,7 +767,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
755767 this . currentBreakpoint = breakpoint ;
756768 this . ionBreakpointDidChange . emit ( { breakpoint } ) ;
757769 }
758- }
770+ this . updateSafeAreaOverrides ( ) ;
771+ } ,
772+ ( ) => this . updateSafeAreaOverrides ( )
759773 ) ;
760774
761775 this . gesture = gesture ;
@@ -849,6 +863,31 @@ export class Modal implements ComponentInterface, OverlayInterface {
849863 this . cachedPageParent = undefined ;
850864 }
851865
866+ /**
867+ * Updates safe-area CSS variable overrides based on whether the modal
868+ * is touching each edge of the viewport. This ensures that modals which
869+ * don't touch an edge (e.g., centered dialogs) don't have unnecessary
870+ * safe-area padding, while full-screen modals properly respect safe areas.
871+ */
872+ private updateSafeAreaOverrides ( ) {
873+ const wrapper = this . wrapperEl ;
874+ if ( ! wrapper ) return ;
875+
876+ const rect = wrapper . getBoundingClientRect ( ) ;
877+ const threshold = 2 ; // Account for subpixel rendering
878+
879+ const touchingTop = rect . top <= threshold ;
880+ const touchingBottom = rect . bottom >= window . innerHeight - threshold ;
881+ const touchingLeft = rect . left <= threshold ;
882+ const touchingRight = rect . right >= window . innerWidth - threshold ;
883+
884+ // Zero out safe-area for edges not touching the viewport (null removes the override)
885+ this . el . style . setProperty ( '--ion-safe-area-top' , touchingTop ? null : '0px' ) ;
886+ this . el . style . setProperty ( '--ion-safe-area-bottom' , touchingBottom ? null : '0px' ) ;
887+ this . el . style . setProperty ( '--ion-safe-area-left' , touchingLeft ? null : '0px' ) ;
888+ this . el . style . setProperty ( '--ion-safe-area-right' , touchingRight ? null : '0px' ) ;
889+ }
890+
852891 private sheetOnDismiss ( ) {
853892 /**
854893 * While the gesture animation is finishing
0 commit comments