HarmonyOS Next: Building Persistent Tabs Using the Navigation Component
Source: Dev.to

Introduction
If you’re building apps with HarmonyOS Next and ArkUI/ArkTS, you know how important a smooth user experience is. One common challenge developers face is keeping the tab bar visible even when users navigate to different screens.
In this article we’ll:
- Explain why tabs disappear when using the router.
- Show a robust solution that combines the Tabs and Navigation components.
The Problem: Tabs Disappearing on Navigation
In ArkUI we often use a Tabs component to create sections such as Home, Profile, Settings, etc. The tabs sit at the bottom of the screen, allowing users to switch between main views.
Everything works fine when you only switch tabs. The issue appears when you click a button inside a tab that navigates to a new page using ArkTS’s router.

Why does this happen?
The Tabs component is usually placed on a single page. When you use a router to jump to another page, you leave the original page that hosts the tabs, causing the tab bar to disappear. Users then lose a key navigation element, which can be confusing.
We want the tabs to stay visible no matter how deep the user goes into a particular section of the app.
The Solution: Combining Tabs with Navigation
HarmonyOS Next provides a better approach: use the Tabs component together with the Navigation component. This duo lets you:
- Keep the tab bar fixed at the bottom.
- Give each tab its own independent navigation stack.
Thus, you can navigate to a detailed screen within a tab while the tab bar remains on screen.
Why Choose Navigation Over Router?
Before diving into code, let’s clarify why Navigation is preferred for this scenario.
“You are advised to use Component Navigation (Navigation), which offers enhanced functionality and customization capabilities, as the routing framework in your application.” – Official HarmonyOS documentation
- Router – Switches between different pages.
- Navigation – Handles component‑level routing inside a single page.
Because the persistent‑tab pattern requires staying on the same page (the one that contains the tabs), Navigation is the right tool. It changes only the content area of a tab, leaving the tab bar untouched, and gives you finer control over complex UI flows.
Building the Persistent Tab UI: Step‑by‑Step
Step 1 – Set Up the Main Tabs Structure
Create the entry component that hosts the Tabs component. This will be the root of the application.
// Index.ets (Main Tabs Component)
@Entry
@Component
export struct Index {
build() {
Tabs({ barPosition: BarPosition.End }) { // Tabs at the bottom
// Tab A – will use Navigation for its content
TabContent() {
NavigationExample() // Custom component for Tab A
}.tabBar('A') // Tab label
// Tab B – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}.tabBar('B')
// Tab C – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#FFBF00')
}.tabBar('C')
// Tab D – simple placeholder
TabContent() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#E67C92')
}.tabBar('D')
}.barBackgroundColor('#ffb9fdf4') // Tab bar background color
}
}
Key points
@Entrymarks this component as the app’s entry point.Tabs({ barPosition: BarPosition.End })creates a bottom tab bar.- Tab A contains the custom
NavigationExamplecomponent, which will manage its own navigation stack. - Tabs B‑D are simple colored columns used for demonstration.
Step 2 – Implement Navigation Inside a Tab
Now create the component that will be placed inside Tab A. It will hold its own navigation history.
// NavigationExample.ets (Content for Tab A)
@Component
struct NavigationExample {
// Dummy data for a simple list
private arr: number[] = [1, 2, 3];
// Provide a NavPathStack so child pages can access navigation state
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
// -------------------------------------------------
// Step 2.1 – Define the Navigation container
// -------------------------------------------------
build() {
Navigation({
// Optional: customize transition animations, etc.
}) {
// Page 1 – List view
Page() {
Column() {
ForEach(this.arr, (item) => {
Button(`Item ${item}`)
.onClick(() => {
// Push a new page onto the navigation stack
this.pageInfos.push({
name: `DetailPage${item}`,
params: { id: item }
});
});
})
}
}
// Page 2 – Detail view (generated dynamically)
Page({ name: (info) => info.name }) {
Column() {
Text(`Detail for item ${this.pageInfos.current?.params?.id}`)
Button('Back')
.onClick(() => this.pageInfos.pop());
}
}
}
}
}
Explanation
@Provide('pageInfos')creates a NavPathStack instance that stores the navigation history for this tab.- The
Navigationcomponent defines a stack‑based navigation flow:- Page 1 shows a list of items. Clicking an item pushes a detail page onto the stack.
- Page 2 displays the selected item’s details and provides a Back button that pops the stack.
Because the navigation occurs inside the NavigationExample component, the Tabs component (outside of it) never disappears.
Recap
| ✅ What we achieved | 📌 How we did it |
|---|---|
| Persistent bottom tab bar | Placed Tabs at the app root (Index.ets) |
| Independent navigation per tab | Used Navigation inside a tab (NavigationExample.ets) |
| Seamless deep navigation without losing tabs | Managed navigation state with NavPathStack and @Provide |
Navigation Destinations
// This @Builder function tells Navigation what content to show for each path.
@Builder
pageMap(name: string) {
NavDestination() {
if (name === '1') {
Text("NavDestinationContent " + name)
} else if (name === '2') { // Example detail page for '2'
Text("Detail Content for Item " + name)
} else if (name === '3') { // Example detail page for '3'
Text("Another Detail Content for Item " + name)
} else {
// Fallback for other items, showing their number
Text("NavDestination Content for Item " + name)
}
}
.title("NavDestinationTitle " + name) // Title for the navigation bar
.onShown(() => {
console.info("show NavDestinationTitle " + name)
})
.onHidden(() => {
console.info("hide NavDestinationTitle " + name)
})
}
// Step 2.2: Building the Navigation Container and its Initial Content
build() {
Column() {
// The Navigation component itself
Navigation(this.pageInfos) { // It uses our NavPathStack for managing history
List({ space: 12 }) { // Initial content: A list of items
ForEach(this.arr, (item: number) => {
ListItem() {
Button("Go to Child " + item)
.width('100%')
.onClick(() => {
// When button is clicked, push a new path onto the Navigation stack
this.pageInfos.pushPathByName(item.toString(), '')
})
}
})
}
}
.navDestination(this.pageMap) // Link Navigation to our pageMap for destination definitions
}
.height('100%')
.width('100%')
}
Breaking Down NavigationExample’s Key Parts
-
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()–
This line is crucial.NavPathStackis whatNavigationuses to manage its screen history within this tab.@ProvideensuresNavigationis set up correctly. -
pageMap([@builder](https://dev.to/builder))–
Think of this as a content map. It tellsNavigationwhat UI (NavDestination) to display for each specific “path” you define (e.g.,'2'or'3'). -
Navigation(this.pageInfos) { … }–
This is yourNavigationcontainer. Its initial content (what’s inside{}) is the first thing users see in this tab. -
Button().onClick(…)–
Tapping a button callsthis.pageInfos.pushPathByName(), adding a new “screen” (frompageMap) toNavigation’s stack. This shows new content without leaving your main tab.
Output
The Result: A Seamless User Experience
With this setup, when a user is on Tab A, they see a list. If they click “Go to Child 1,” they’ll see the detail content for Child 1 at the top of the screen, but the tab bar (A, B, C, D) will still be visible at the bottom. They can then easily go back within Tab A’s history or switch to Tab B, C, or D without losing their context.
Advantages of This Approach
- Always‑Visible Tabs – Your app’s main tabs remain on screen, offering consistent navigation.
- Independent Navigation – Each tab can have its own separate history stack, making complex apps easier to manage and understand.
- Better User Flow – Users can explore deep content within a tab and then switch to another main section without feeling lost.
- Clean Code – By separating navigation logic per tab, your codebase becomes more organized and maintainable.
Conclusion
By intelligently combining HarmonyOS Next’s Tabs and Navigation components, you can overcome the common challenge of disappearing tab bars. This approach makes it easy and powerful to build apps with UI elements that stay put, greatly improving the overall user experience.
References
Written by Muhammet Ali Ilgaz
