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