Custom dialogs are essential in mobile application interfaces, covering modal, semi-modal, toast, and other overlay styles. A well-designed dialog component should be independent of specific UI pages and easily triggered from business logic.
Common real-world needs include:
- Triggering dialogs from shared services (login prompts, full-screen ads, network alerts).
- Intercepting back gestures or swipe actions (privacy consent, logout confirmations).
- Keeping a dialog visible across page navigations.
- Applying custom enter/exit transitions (drawer-style animations).
- Using transparent or semi-transparent backdrops.
Leveraging Navigation.Dialog for Custom Overlays
Navigation.Dialog behaves like a tarnsparent page within the naivgation stack, making it a natural fit for custom dialog implementations while preserving the ability to persist across page changes.
Key considerations:
- Animation effects inside the dialog must be handled manually by the developer.
- The dialog lacks built-in background shading; developers need to construct a mask layer and manage gesture interception.
Implementation Steps
1. Set Up a Router with NavPathStack
Create a centralized router singleton that wraps NavPathStack.
export class NavigationRouter {
private static instance: NavigationRouter;
private routeStack: NavPathStack = new NavPathStack();
static getInstance(): NavigationRouter {
if (!NavigationRouter.instance) {
NavigationRouter.instance = new NavigationRouter();
}
return NavigationRouter.instance;
}
getStack(): NavPathStack {
return this.routeStack;
}
// Additional helper methods for pushing dialog pages...
}
2. Initialize the Navigation Container
In the root component, bind the shared stack to a Navigation component.
@Entry
@Component
struct AppRoot {
build() {
Navigation(NavigationRouter.getInstance().getStack()) {
// Main content area
}
}
}
3. Register Custom Dialog in .navDestination
Map a route name to your custom dialog component using the navDestination builder. This example uses a BaseDialog as the wrapper.
@Builder
buildPageMap(name: string) {
if (name === 'CUSTOM_DIALOG_ROUTE') {
BaseDialog()
}
// ... other pages
}
Navigation(NavigationRouter.getInstance().getStack()) {
// ...
}
.navDestination(this.buildPageMap)
4. Define the Custom Dialog Componetn
Design BaseDialog as a stateful component that renders its content over a tap-through prevention layer.
@Component
export struct BaseDialog {
private navController: NavDestinationController = new NavDestinationController();
@State showContent: boolean = false;
aboutToAppear() {
// Animate content appearance
this.showContent = true;
}
build() {
NavDestination() {
Stack() {
// Semi-transparent backdrop that blocks interactions
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
// Optional: close dialog on backdrop tap
})
// Actual dialog panel with custom animation
Column() {
// Dialog content goes here
Text('Custom Dialog Content')
Button('Dismiss')
.onClick(() => {
this.navController.pop();
})
}
.width('80%')
.height('40%')
.backgroundColor(Color.White)
.borderRadius(16)
.translate({
y: this.showContent ? 0 : 200,
})
.animation({ duration: 300, curve: Curve.EaseOut })
}
.width('100%')
.height('100%')
}
.hideTitleBar(true)
}
}
With this setup, any business logic can trigger the dialog by pushing CUSTOM_DIALOG_ROUTE onto the shared NavPathStack. The dialog remains visible even if the underlying page navigates to a sub-page, and developers have full control over visual styling and animation.