This document outlines the architecture and data flow for handling display cutouts (i.e., “notches”) and their associated safe area insets in Chromium. This mechanism allows web content to utilize the entire screen on devices with cutouts, using web standards like the viewport-fit meta tag and the env() CSS function.
Relevant classes:
viewport-fit: A CSS descriptor, specified via a <meta> tag, that controls how a web page is displayed relative to the safe area. Read moreauto (default): The web page is confined to the safe area.contain: The web page is scaled to fit entirely within the safe area.cover: The web page is scaled to fill the entire viewport, including the area “under” the cutout. This is used to create an “edge-to-edge” experience.viewport-fit=cover will draw under the display cutout, while pages with other values will not.Useful resources:
The implementation spans three main layers of the Chromium stack: Blink, C++ browser, Java UI. The entire process involves a round trip of information. The sequence diagrams illustrating this data flow can be found below.
Source see Data flow source.
Parses the viewport-fit meta tag and uses the safe area insets provided by the browser to resolve the env() CSS function.
//content/browser/display_cutout)Acts as the central hub for display cutout information. - From Blink to Java, it tracks the viewport-fit state for each web page and notifies the UI layer to adjust the Android window layout. - From Java to Blink, it funnels the Android window inset data back to the renderer.
//chrome/android and //components/browser_ui)Interacts with the Android OS to get cutout information, sets the appropriate window flags to enable drawing under the cutout or into system bars, and provides the safe area inset values to the C++ layer.
<meta name="viewport" content="viewport-fit=..."> tag.DisplayCutoutHost Mojo interface.[2] SafeAreaInsetsHostImpl (//content/browser/display_cutout)
blink::mojom::DisplayCutoutHost, receiving NotifyViewportFitChanged calls from Blink.DocumentUserData to correctly associate a viewport-fit value with each document.WebContentsImpl::NotifyViewportFitChanged() to notify observers of the change.WebContentObserver.viewportFitChanged() and forwards the info to DisplayCutoutTabHelper.[4] DisplayCutoutTabHelper (//chrome/android/java/src/org/chromium/chrome/browser/display_cutout)
UserData scoped to a Tab, and creates and owns the DisplayCutoutController.DisplayCutoutController.Delegate to provide the controller with access to tab-specific objects like WebContents and WindowAndroid.DisplayCutoutController.[5] DisplayCutoutController (//components/browser_ui/display_cutout) - Calculates the appropriate LayoutParams.layoutInDisplayCutoutMode window flag based on the page's viewport-fit value and other states (e.g., fullscreen). - viewport-fit=cover → LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - viewport-fit=contain → LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - Applies this flag to the Activity window, telling Android to let the app draw behind the display cutout.
[1] InsetObserver (//ui/android/java/src/org/chromium/ui/insets/)
WindowInsetObserver.onSafeAreaChanged()[2] DisplayCutoutController (//components/browser_ui/display_cutout)
WindowInsetObserver.onSafeAreaChanged() to receive safe area updates calculated based on the Android Window Insets.InsetObserver, it scales the pixels into dips and pushes them to the C++ layer via a JNI call (WebContents.setDisplayCutoutSafeArea()).//content/browser/display_cutout)SetDisplayCutoutSafeArea() is called from the Java layer, it sends the inset values to the active frame in Blink via the blink::mojom::DisplayCutoutClient interface.safe-area-inset-*. At all times, only one frame will receive the safe-area-inset.safe-area-inset-* values in the previous active frame will be reset to 0.[4] blink::DisplayCutoutClientImpl (//third_party/blink/renderer/core/frame)
gfx::Insets from the browser process via the DisplayCutoutClient interface.gfx::Insets to the current blink::Page::SetMaxSafeAreaInsets.[5] blink::Page (//third_party/blink/renderer/core/page/)
gfx::Insets and stores them as scaled_max_safe_area_insets_.DynamicSafeAreaInsetsEnabled setting is active, the final applied safe area can be adjusted dynamically. The function UpdateSafeAreaInsetWithBrowserControls calculates the effective bottom inset by considering the height and visibility of the browser's bottom controls (like a navigation bar). As the browser controls slide into view, the bottom safe area inset is reduced, and as they slide out, the inset is increased.SetSafeAreaEnvVariables function is called to update the CSS env() variables. This updates safe-area-inset-* and safe-area-max-inset-*.viewport-fit: cover<meta name="viewport" content="viewport-fit=cover">.cover to the browser process via a DisplayCutoutHost Mojo call.SafeAreaInsetsHostImpl (C++) receives the value. It stores viewport-fit=cover for the document and notifies WebContentsImpl of the change.WebContentsImpl propagates this notification to its Java observers.DisplayCutoutTabHelper (Java) receives the cover value and passes it to the DisplayCutoutController.DisplayCutoutController (Java), seeing cover and assuming the tab is fullscreen, sets the Activity window's layoutInDisplayCutoutMode to LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES.InsetObserver.DisplayCutoutController's onSafeAreaChanged method is triggered. It calls WebContents.setDisplayCutoutSafeArea() with the new insets.SafeAreaInsetsHostImpl (C++), which forwards the insets to the active RenderFrameHost in Blink.padding-top: env(safe-area-inset-top);, is now correctly resolved, and the env() function returns 48px. The page can now adjust its layout to avoid the display cutout.In certain scenarios, such as when Chrome renders edge-to-edge content outside of fullscreen mode, the safe area insets must be adjusted dynamically. This is necessary to account for the presence of browser controls (e.g., the top and bottom toolbars), which can overlay the web content.
As a user scrolls the page, the browser controls retract to offer a more immersive experience. This retraction changes the size of the viewport and, crucially, the safe area available to the web content. The safe-area-inset-* CSS environment variables are updated dynamically to reflect these changes.
For example, as the bottom browser toolbar slides into view, the safe-area-inset-bottom value decreases, ensuring that web content repositions itself to remain accessible. Conversely, as the toolbar slides out of view, the inset increases.
To provide web developers with a stable value for layout calculations, safe-area-max-inset-* has been introduced. This variable represents the maximum possible inset value, which is the state when the browser controls are fully retracted. This allows developers to design layouts against a consistent and predictable safe area boundary.
safe-area-max-inset-*, see the CSSWG discussion for more details.//docs/ui/android/browser_controls.md.@startuml
title Display Cutout Data Flow
box "Blink (Renderer)" #LightBlue
participant "ViewportData" as ViewportData
participant "DisplayCutoutClientImpl" as CutoutClient
participant "Page" as Page
end box
box "C++ Browser" #LightGreen
participant "SafeAreaInsetsHostImpl" as InsetsHost
end box
box "Java UI" #LightYellow
participant "TabWebContentsObserver" as TabObserver
participant "DisplayCutoutTabHelper" as TabHelper
participant "DisplayCutoutController" as CutoutController
participant "InsetObserver" as InsetObserver
end box
participant "Android Window" as AndroidOS
== Flow 1: viewport-fit value (Blink to Java) ==
autonumber "<b>[1]"
activate ViewportData
ViewportData -> InsetsHost: NotifyViewportFitChanged()
deactivate ViewportData
activate InsetsHost
InsetsHost -> TabObserver: WebContentsObserver.viewportFitChanged()
deactivate InsetsHost
activate TabObserver
TabObserver -> TabHelper: forwards viewport-fit
deactivate TabObserver
activate TabHelper
TabHelper -> CutoutController: forwards viewport-fit
deactivate TabHelper
activate CutoutController
CutoutController -> AndroidOS: Set Window LayoutParams
deactivate CutoutController
== Flow 2: safe-area-inset value (Java to Blink) ==
autonumber "<b>[2]"
activate AndroidOS
AndroidOS -> InsetObserver: Window Insets Changed
deactivate AndroidOS
activate InsetObserver
InsetObserver -> CutoutController: onSafeAreaChanged()
deactivate InsetObserver
activate CutoutController
CutoutController -> InsetsHost: setDisplayCutoutSafeArea() [JNI]
deactivate CutoutController
activate InsetsHost
InsetsHost -> CutoutClient: SetSafeArea()
deactivate InsetsHost
activate CutoutClient
CutoutClient -> Page: SetMaxSafeAreaInsets()
note right of Page
Page receives insets and
updates CSS env() variables.
end note
deactivate CutoutClient
@enduml