@@ -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 }
@@ -592,6 +595,15 @@ export class Modal implements ComponentInterface, OverlayInterface {
592595 await waitForMount ( ) ;
593596 }
594597
598+ // Set all safe-areas to 0 before modal becomes visible to prevent flash
599+ // for centered dialogs. After animation, updateSafeAreaOverrides() will
600+ // restore values for edges that touch the viewport.
601+ const style = this . el . style ;
602+ style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
603+ style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
604+ style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
605+ style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
606+
595607 writeTask ( ( ) => this . el . classList . add ( 'show-modal' ) ) ;
596608
597609 const hasCardModal = presentingElement !== undefined ;
@@ -659,6 +671,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
659671 this . initSwipeToClose ( ) ;
660672 }
661673
674+ // Now that animation is complete, update safe-area based on actual position
675+ this . updateSafeAreaOverrides ( ) ;
676+
662677 // Initialize view transition listener for iOS card modals
663678 this . initViewTransitionListener ( ) ;
664679
@@ -692,33 +707,39 @@ export class Modal implements ComponentInterface, OverlayInterface {
692707
693708 const statusBarStyle = this . statusBarStyle ?? StatusBarStyle . Default ;
694709
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- } ) ;
710+ this . gesture = createSwipeToCloseGesture (
711+ el ,
712+ ani ,
713+ statusBarStyle ,
714+ ( ) => {
715+ /**
716+ * While the gesture animation is finishing
717+ * it is possible for a user to tap the backdrop.
718+ * This would result in the dismiss animation
719+ * being played again. Typically this is avoided
720+ * by setting `presented = false` on the overlay
721+ * component; however, we cannot do that here as
722+ * that would prevent the element from being
723+ * removed from the DOM.
724+ */
725+ this . gestureAnimationDismissing = true ;
726+
727+ /**
728+ * Reset the status bar style as the dismiss animation
729+ * starts otherwise the status bar will be the wrong
730+ * color for the duration of the dismiss animation.
731+ * The dismiss method does this as well, but
732+ * in this case it's only called once the animation
733+ * has finished.
734+ */
735+ setCardStatusBarDefault ( this . statusBarStyle ) ;
736+ this . animation ! . onFinish ( async ( ) => {
737+ await this . dismiss ( undefined , GESTURE ) ;
738+ this . gestureAnimationDismissing = false ;
739+ } ) ;
740+ } ,
741+ ( ) => this . updateSafeAreaOverrides ( )
742+ ) ;
722743 this . gesture . enable ( true ) ;
723744 }
724745
@@ -755,7 +776,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
755776 this . currentBreakpoint = breakpoint ;
756777 this . ionBreakpointDidChange . emit ( { breakpoint } ) ;
757778 }
758- }
779+ this . updateSafeAreaOverrides ( ) ;
780+ } ,
781+ ( ) => this . updateSafeAreaOverrides ( )
759782 ) ;
760783
761784 this . gesture = gesture ;
@@ -849,6 +872,34 @@ export class Modal implements ComponentInterface, OverlayInterface {
849872 this . cachedPageParent = undefined ;
850873 }
851874
875+ /**
876+ * Updates safe-area CSS variable overrides based on whether the modal
877+ * is touching each edge of the viewport. This ensures that modals which
878+ * don't touch an edge (e.g., centered dialogs) don't have unnecessary
879+ * safe-area padding, while full-screen modals properly respect safe areas.
880+ */
881+ private updateSafeAreaOverrides ( ) {
882+ const wrapper = this . wrapperEl ;
883+ if ( ! wrapper ) return ;
884+
885+ const rect = wrapper . getBoundingClientRect ( ) ;
886+ const threshold = 2 ; // Account for subpixel rendering
887+
888+ const touchingTop = rect . top <= threshold ;
889+ const touchingBottom = rect . bottom >= window . innerHeight - threshold ;
890+ const touchingLeft = rect . left <= threshold ;
891+ const touchingRight = rect . right >= window . innerWidth - threshold ;
892+
893+ // Remove override when touching edge (allow inheritance), set to 0 when not touching
894+ const style = this . el . style ;
895+ touchingTop ? style . removeProperty ( '--ion-safe-area-top' ) : style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
896+ touchingBottom
897+ ? style . removeProperty ( '--ion-safe-area-bottom' )
898+ : style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
899+ touchingLeft ? style . removeProperty ( '--ion-safe-area-left' ) : style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
900+ touchingRight ? style . removeProperty ( '--ion-safe-area-right' ) : style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
901+ }
902+
852903 private sheetOnDismiss ( ) {
853904 /**
854905 * While the gesture animation is finishing
0 commit comments