HarmonyOS Next: Building Persistent Tabs Using the Navigation Component



This content originally appeared on DEV Community and was authored by HarmonyOS

Read the original article:HarmonyOS Next: Building Persistent Tabs Using the Navigation Component

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 making sure your app’s tab bar stays visible even when users navigate to different screens.

In this article, we’ll look at this problem and then show you a powerful solution using the Tabs and Navigation components together.

The Problem: Tabs Disappearing on Navigation
In ArkUI, we often use a Tabs Component to create different sections in our app, like “Home,” “Profile,” “Settings,” etc. These tabs usually sit at the bottom of the screen, letting users easily switch between main views.

Everything works great when you’re just switching tabs. But what happens when you click a button inside one of these tabs, and that button takes you to a new page by using ArkTS`s router?

Here`s the catch: the Tabs Component itself is typically part of a single page. So, when you use a router to jump to a completely new page, you actually leave the original page where your tabs were placed. This makes your tab bar disappear, which can be confusing for users. We want the tabs to always be there, no matter how deep the user goes into a particular section of the app.

The Solution: Combining Tabs with Navigation
HarmonyOS Next offers a much better way to handle this: by using the Tabs component alongside the Navigation component. This powerful duo lets you keep your tab bar fixed at the bottom while allowing each tab to have its own separate, deep navigation flow.

This means you can tap on an item within a tab, go to a detailed screen, and still see your tabs at the bottom.

Why Choose Navigation Over Router?
Let`s talk about some differences between navigation and router before we dive into the code, it’s important to understand why Navigation is the preferred choice here. The official HarmonyOS documentation says:

You are advised to use Component Navigation (Navigation), which offers enhanced functionality and customization capabilities, as the routing framework in your application.

While Router helps you switch between different pages, Navigation is designed for component-level routing within a single page or view. This is crucial for our persistent tab scenario. With Navigation, you’re not leaving the main page (and thus the Tabs component); instead, you’re changing the content within a specific area, keeping your tabs untouched. This gives you more control and a smoother flow for complex UIs.

Building the Persistent Tab UI: Step-by-Step
Now, let`s see how this all comes together in code.

Step 1: Setting Up the Main Tabs Structure
First, we define our main application entry point and set up the Tabs component. This will be the root of our application.

// Index.ets (Main Tabs Component)
@Entry
@Component
export struct Index {
  build() {
    Tabs({ barPosition: BarPosition.End }) { // Tabs at the bottom
      // Tab A: This tab will use Navigation for its content
      TabContent() {
        NavigationExample() // Our custom component for tab A's content
      }.tabBar('A') // The label for Tab A

      // Tab B: Simple content for demonstration
      TabContent() {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('#007DFF')
      }.tabBar('B')

      // Tab C: Simple content
      TabContent() {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('#FFBF00')
      }.tabBar('C')

      // Tab D: Simple content
      TabContent() {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('#E67C92')
      }.tabBar('D')
    }.barBackgroundColor('#ffb9fdf4') // Background color for the tab bar
  }
}

In this Index component:

  • We use @Entry to make it the starting point of our app.

  • Tabs({ barPosition: BarPosition.End }) creates the tab bar at the bottom.

  • Each TabContent defines a single tab. Notice that for Tab ‘A’, we’ve placed our custom component NavigationExample() . This is where the magic happens for nested navigation. The other tabs just have simple colored columns for now.
    Step 2: Implementing Navigation within a Tab
    Now, lets create the NavigationExample component. This component will be placed inside one of our tabs (Tab A, in this case) and will manage its own navigation history.

// NavigationExample.ets (Content for Tab A)
@Component
struct NavigationExample {
  private arr: number[] = [1, 2, 3]; // Some dummy data for our list
  // @Provide for state management to pass NavPathStack
  @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();

  // Step 2.1: Defining 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 '4'
        Text("Detail Content for Item " + name)
      } else if (name === '3') { // Example detail page for '5'
        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%')
  }
}

Lets break down NavigationExample’s key parts:

  • @Provide(‘pageInfos’) pageInfos: NavPathStack = new NavPathStack(): This line is crucial. NavPathStack is what Navigation uses to manage its screen history within this tab. @Provide ensures Navigation is set up correctly.

  • pageMap(@builder): Think of this as a content map. It tells Navigation what UI ( NavDestination ) to display for each specific “path” you define (e.g., ‘4’ or ‘5’).

  • Navigation(this.pageInfos) { … }: This is your Navigation container. Its initial content (what’s inside {}) is the first thing users see in this tab.

  • Button().onClick(…): Tapping a button calls this.pageInfos.pushPathByName(), adding a new “screen” (from pageMap) to Navigation’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,” theyll 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 As 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. This makes complex apps easier to manage and understand.

  • Better User Flow: Users can explore deep content within a tab and then easily 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, making your app much better to use.

References
https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V5/arkts-router-to-navigation-V5?source=post_page—–ab99cfc5397d—————————————

https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V5/arkts-routing-V5?source=post_page—–ab99cfc5397d—————————————

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-basic-components-navigation?source=post_page—–ab99cfc5397d—————————————

https://developer.huawei.com/consumer/en/doc/harmonyos-references/ts-container-tabs?source=post_page—–ab99cfc5397d—————————————

Written by Muhammet Ali Ilgaz


This content originally appeared on DEV Community and was authored by HarmonyOS