@@ -595,14 +595,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
595595 await waitForMount ( ) ;
596596 }
597597
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' ) ;
598+ // Predict safe-area needs based on modal configuration to avoid visual snap
599+ this . setInitialSafeAreaOverrides ( presentingElement ) ;
606600
607601 writeTask ( ( ) => this . el . classList . add ( 'show-modal' ) ) ;
608602
@@ -872,11 +866,63 @@ export class Modal implements ComponentInterface, OverlayInterface {
872866 this . cachedPageParent = undefined ;
873867 }
874868
869+ /**
870+ * Sets initial safe-area overrides based on modal configuration before
871+ * the modal becomes visible. This predicts whether the modal will touch
872+ * screen edges to avoid a visual snap after animation completes.
873+ */
874+ private setInitialSafeAreaOverrides ( presentingElement : HTMLElement | undefined ) {
875+ const style = this . el . style ;
876+ const isSheetModal = this . breakpoints !== undefined && this . initialBreakpoint !== undefined ;
877+ const isCardModal = presentingElement !== undefined ;
878+ const isTablet = window . innerWidth >= 768 ;
879+
880+ // Sheet modals: always touch bottom, top depends on breakpoint
881+ if ( isSheetModal ) {
882+ style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
883+ // Don't override bottom - sheet always touches bottom
884+ style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
885+ style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
886+ return ;
887+ }
888+
889+ // Card modals are inset from edges (rounded corners), no safe areas needed
890+ if ( isCardModal ) {
891+ style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
892+ style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
893+ style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
894+ style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
895+ return ;
896+ }
897+
898+ // Phone modals are fullscreen, need all safe areas
899+ if ( ! isTablet ) {
900+ // Don't set any overrides - inherit from :root
901+ return ;
902+ }
903+
904+ // Default tablet modal: centered dialog, no safe areas needed
905+ // Check for fullscreen override via CSS custom properties
906+ const computedStyle = getComputedStyle ( this . el ) ;
907+ const width = computedStyle . getPropertyValue ( '--width' ) . trim ( ) ;
908+ const height = computedStyle . getPropertyValue ( '--height' ) . trim ( ) ;
909+
910+ if ( width === '100%' && height === '100%' ) {
911+ // Fullscreen modal - need safe areas, don't override
912+ return ;
913+ }
914+
915+ // Centered dialog - zero out all safe areas
916+ style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
917+ style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
918+ style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
919+ style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
920+ }
921+
875922 /**
876923 * 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.
924+ * is touching each edge of the viewport. This is called after animation
925+ * and during gestures to handle dynamic position changes.
880926 */
881927 private updateSafeAreaOverrides ( ) {
882928 const wrapper = this . wrapperEl ;
0 commit comments