Using Tabs (Tabs)
When there is a large amount of page information, to enable the user to focus on the currently displayed content, the page content needs to be classified to improve the page space utilization. The Tabs component can quickly switch between views on a page, improving information search efficiency and reducing the amount of information that users receive at a time.
Basic Layout
The Tabs component consists of two parts: TabContent and TabBar. TabContent is the content page, and TabBar is the navigation tab bar. The following figure shows the page structure. The layout varies according to the navigation type. In bottom navigation, top navigation, and side navigation, the navigation tab bar is located at the bottom, top, and edge, respectively.
Figure 1 Tabs component layout

NOTE
The TabContent component does not support setting of the common width attribute. By default, its width is the same as that of the parent Tabs component.
The TabContent component does not support setting of the common height attribute. Its height is determined by the height of the parent Tabs component and the TabBar component.
The Tabs component uses braces to wrap TabContent child components, as shown in Figure 2.
Figure 2 Using Tabs and TabContent

Each TabContent component should be mapped to a tab page, which can be configured through the tabBar attribute. The following is an example.
TabContent() {
// The value in the app.string.homepage_content resource file is "Home tab content."
Text($r('app.string.homepage_content'))
.fontSize(30)
}
// The value in the app.string.homepage resource file is "Home."
.tabBar($r('app.string.homepage'))
When setting multiple TabContent components, place them in sequence in the Tabs component.
Tabs() {
TabContent() {
// The value in the app.string.homepage_content resource file is "Home tab content."
Text($r('app.string.homepage_content'))
.fontSize(30)
}
// The value in the app.string.homepage resource file is "Home."
.tabBar($r('app.string.homepage'))
TabContent() {
// The value in the app.string.recommend_content resource file is "Recommended tab content."
Text($r('app.string.recommend_content'))
.fontSize(30)
}
// The value in the app.string.recommend resource file is "Recommended."
.tabBar($r('app.string.recommend'))
TabContent() {
// The value in the app.string.discover_content resource file is "Discover tab content."
Text($r('app.string.discover_content'))
.fontSize(30)
}
// The value in the app.string.discover resource file is "Discover."
.tabBar($r('app.string.discover'))
TabContent() {
// The value in the app.string.mine_content resource file is "Me tab content."
Text($r('app.string.mine_content'))
.fontSize(30)
}
// The value in the app.string.mine_content resource file is "Me."
.tabBar($r('app.string.mine'))
}
Bottom Navigation
Bottom navigation is the most common navigation mode in applications. The bottom navigation bar is located at the bottom of the level-1 page of the application. It enables the user to quickly have a picture of the feature categories the moment they open the application. In addition, it facilitates one-hand operations of the user. Bottom navigation generally exists as a main navigation form of an application, in that it provides convenient access to primary destinations anywhere in the application.
Figure 3 Bottom navigation bar

The position of the navigation bar is specified by the barPosition attribute of Tabs. By default, barPosition is set to BarPosition.Start, which means that the navigation bar is located on the top. To display the navigation bar at the bottom, set barPosition to BarPosition.End.
Tabs({ barPosition: BarPosition.End }) {
// TabContent: Home, Discover, Recommended, and Me
// ···
}
You can customize the appearance of the bottom navigation bar by setting the BottomTabBarStyle attribute of TabContent. For details, see Example 8: Using Symbol Icons for Bottom Tabs.
Top Navigation
Top navigation comes in handy when there are many content categories and users need to frequently switch between them. It is usually a further subdivision of the categories in the bottom navigation bar. For example, a theme application may provide a top navigation bar that classifies themes into image, video, and font.
Figure 4 Top navigation bar

Tabs({ barPosition: BarPosition.Start }) {
// TabContent: Following, Video, Game, Digital, Technology, Sports, Movie
// ···
}
Side Navigation
Side navigation is seldom used in applications. It is more applicable to landscape screens. Because the natural eye movement pattern is from left to right, the side navigation bar is located on the left side by default.
Figure 5 Side navigation bar

To implement the side navigation bar, set the vertical attribute of the Tabs component to true. By default, vertical is set to false, indicating that the content page and navigation bar are aligned vertically.
Tabs({ barPosition: BarPosition.Start }) {
// TabContent: Home, Discover, Recommended, and Me
// ···
}
// ···
.vertical(true)
.barWidth(100)
.barHeight(200)
NOTE
Restricting the Scrolling of the Navigation Bar
By default, the navigation bar is scrollable. On some pages that require multi-level classification of content, for example, when both bottom navigation and top navigation are used, the scroll effect of the bottom navigation bar may conflict with that of the top navigation bar. In this case, the scrolling of the bottom navigation bar needs to be restricted to improve user experience.
Figure 6 Restricting the scrolling of the bottom navigation bar

The attribute that controls the scrolling is scrollable. The default value is true, indicating that scrolling is allowed. To restrict swipe-to-switch tabs, set this attribute to false.
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
Column() {
Tabs() {
// Content on the top navigation bar
// ···
}
}
.backgroundColor('#ff08a8f1')
.width('100%')
}
// The value in the app.string.homepage resource file is "Home."
.tabBar($r('app.string.homepage'))
// Other TabContent content: Discover, Recommended, and Me
// ···
}
// ···
.scrollable(false)
Fixed Navigation Bar
When the content categories are relatively fixed and not scalable, a fixed navigation bar can be used. For example, it can be used for the bottom navigation bar, which generally contains 3 to 5 categories. The fixed navigation bar cannot be scrolled or dragged. The tab bar width is evenly distributed among the categories.
Figure 7 Fixed navigation bar

To use a fixed navigation bar, set the barMode attribute of the Tabs component to BarMode.Fixed (default).
Tabs({ barPosition: BarPosition.End }) {
// TabContent: Home, Discover, Recommended, and Me
// ···
}
.barMode(BarMode.Fixed)
Scrollable Navigation Bar
The top navigation bar or side navigation bar can be set to be scrollable if the screen width cannot fully accommodate all the tabs. With a scrollable navigation bar, users can reveal tabs beyond the visible area by touching or swiping on the navigation bar.
Figure 8 Scrollable navigation bar

To use a scrollable navigation bar, set the barMode attribute of the Tabs component to BarMode.Scrollable.
Tabs({ barPosition: BarPosition.Start }) {
// TabContent: follow, video, game, digital, technology, sports, movie, humanities, art, nature, and military
// ···
}
.barMode(BarMode.Scrollable)
Customizing the Navigation Bar
The bottom navigation bar is generally used on the home page of an application. To deliver a more vibrant experience, you can customize the style of the navigation bar, combining use of text and icons to signify the tab content.
Figure 9 Custom navigation bar

By default, the system uses an underscore (_) to indicate the active tab. For a custom navigation bar, you need to implement the corresponding style to distinguish active tabs from inactive tabs.
To customize the navigation bar, use the tabBar parameter and pass in to it custom function component styles in CustomBuilder mode. In this example, a custom function component tabBuilder is declared, and the input parameters include title (tab title), targetIndex (target index of the tab), selectedImg (image for the selected state), and normalImg (image for the unselected state). The UI display style is determined based on whether the value of currentIndex (index of the active tab) matches that of targetIndex (target index of the tab).
@State currentIndex: number = 0;
@Builder
tabBuilder(title: ResourceStr, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
Pass the custom function component to the tabBar attribute corresponding to the tab content and transfer the corresponding parameters.
TabContent() {
Column() {
// The value in the app.string.mine_content resource file is "Me tab content."
Text($r('app.string.mine_content'))
}
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}
// The value in the app.string.mine resource file is "Me."
.tabBar(this.tabBuilder($r('app.string.mine'), 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
Switching to a Specified Tab
Non-custom navigation bars follow the default switching logic. If you are using a custom navigation bar, you must manually implement the logic for switching tabs so that when the user switches to a tab, the application displays the corresponding tab page.
Figure 10 Content page and tab bar not synced

Since API version 18, the Tabs component supports the onSelected event method. This method listens for index changes and passes the selected element's index value to selectIndex, enabling tab switching functionality.
// To use it as the entry of a page, uncomment @Entry and delete the export keyword.
// @Entry
@Component
export struct ContentPageNoAndTabLinkage {
@State selectIndex: number = 0;
@Builder tabBuilder(title: Resource, targetIndex: number) {
Column() {
Text(title)
.fontColor(this.selectIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
}
build() {
NavDestination() {
Column({ space: 12 }) {
// ...
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
// The value in the app.string.homepage_content resource file is "Home tab content."
Text($r('app.string.homepage_content')).width('100%').height('100%').backgroundColor('rgb(213,213,213)')
.fontSize(40).fontColor(Color.Black).textAlign(TextAlign.Center)
// The value in the app.string.homepage resource file is "Home."
}.tabBar(this.tabBuilder($r('app.string.homepage'), 0))
TabContent() {
// The value in the app.string.discover_content resource file is "Discover tab content."
Text($r('app.string.discover_content')).width('100%').height('100%').backgroundColor('rgb(112,112,112)')
.fontSize(40).fontColor(Color.Black).textAlign(TextAlign.Center)
// The value in the app.string.discover resource file is "Discover."
}.tabBar(this.tabBuilder($r('app.string.discover'), 1))
TabContent() {
// The value in the app.string.recommend_content resource file is "Recommended tab content."
Text($r('app.string.recommend_content')).width('100%').height('100%').backgroundColor('rgb(39,135,217)')
.fontSize(40).fontColor(Color.Black).textAlign(TextAlign.Center)
// The value in the app.string.recommend resource file is "Recommended."
}.tabBar(this.tabBuilder($r('app.string.recommend'), 2))
TabContent() {
// The value in the app.string.mine_content resource file is "Me tab content."
Text($r('app.string.mine_content')).width('100%').height('100%').backgroundColor('rgb(0,74,175)')
.fontSize(40).fontColor(Color.Black).textAlign(TextAlign.Center)
}
// The value in the app.string.mine resource file is "Me."
.tabBar(this.tabBuilder($r('app.string.mine'), 3))
}
.animationDuration(0)
.backgroundColor('#F1F3F5')
.onSelected((index: number) => {
this.selectIndex = index;
})
// ...
}
.width('100%')
// ...
}
// ...
}
}

To enable switching between content pages and tabs without swiping, you can pass currentIndex to the index parameter of Tabs. By changing the value of currentIndex, you can navigate to the content page corresponding to a specific index. Alternatively, use TabsController, which is the controller for the Tabs component, to manage switching of tabs. By using the changeIndex API of TabsController, you can set your application to display the tab content corresponding to the specified index.
// ...
@State currentIndex: number = 2;
@State currentAnimationMode: AnimationMode = AnimationMode.CONTENT_FIRST;
private controller: TabsController = new TabsController();
// ...
Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.controller }) {
// ...
}
.animationDuration(0)
.height(300)
.animationMode(this.currentAnimationMode)
.onChange((index: number) => {
this.currentIndex = index;
})
// The value in the app.string.ContentWillChange_animationMode resource file is "Dynamically Change AnimationMode."
Button($r('app.string.ContentWillChange_animationMode')).width('50%').margin({ top: 20 })
.onClick(()=>{
if (this.currentAnimationMode === AnimationMode.CONTENT_FIRST) {
this.currentAnimationMode = AnimationMode.ACTION_FIRST;
} else if (this.currentAnimationMode === AnimationMode.ACTION_FIRST) {
this.currentAnimationMode = AnimationMode.NO_ANIMATION;
} else if (this.currentAnimationMode === AnimationMode.NO_ANIMATION) {
this.currentAnimationMode = AnimationMode.CONTENT_FIRST_WITH_JUMP;
} else if (this.currentAnimationMode === AnimationMode.CONTENT_FIRST_WITH_JUMP) {
this.currentAnimationMode = AnimationMode.ACTION_FIRST_WITH_JUMP;
} else if (this.currentAnimationMode === AnimationMode.ACTION_FIRST_WITH_JUMP) {
this.currentAnimationMode = AnimationMode.CONTENT_FIRST;
}
})
// The value in the app.string.ContentWillChange_changeIndex resource file is "Dynamically Change Index."
Button($r('app.string.ContentWillChange_changeIndex')).width('50%').margin({ top: 20 })
.onClick(() => {
this.currentIndex = (this.currentIndex + 1) % 4;
})
Button('changeIndex').width('50%').margin({ top: 20 })
.onClick(() => {
let index = (this.currentIndex + 1) % 4;
this.controller.changeIndex(index);
})
Figure 12 Switching to a specific tab page

You can use the onContentWillChange API of the Tabs component to customize the interception callback. The interception callback function is called when a new page is about to be displayed. If the callback returns true, the tab can switch to the new page. If the callback returns false, the tab cannot switch to the new page and will remain on the current page.
Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.controllerTwo }) {
// ...
}
// ...
.onContentWillChange((currentIndex, comingIndex) => {
if (comingIndex === 2) {
return false;
}
return true;
})

Supporting Aging-Friendly Design
In aging-friendly scenarios with large font sizes, the bottom tab bar offers a dialog box with large fonts for content display. When the component detects a large font setting, it constructs a long-press dialog box based on the configured text and icons. After the user long-presses the tab bar and then swipes in the dialog box to switch to the next tab, the dialog box updates with content of the new tab. Upon releasing, the dialog box closes and the UI switches to the corresponding tab page.
NOTE
The dialog box is applicable only to the bottom tab bar BottomTabBarStyle.
Figure 14 Displaying an aging-friendly dialog box by long-pressing the bottom tab bar in an aging-friendly scenario

import { abilityManager, Configuration } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { uiAppearance } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
const TAG: string = 'AgeFriendlyTabs';
// To use it as the entry of a page, uncomment @Entry and delete the export keyword.
// @Entry
@Component
export struct AgeFriendlyTabs {
@State fontColor: string = '#182431';
@State selectedFontColor: string = '#007DFF';
@State currentIndex: number = 0;
@State currentFontSizeScale: string = '';
@State showBuilderTab: boolean = false;
@State fontSize: number = 15;
private darkModeKey: string[] = Object.keys(uiAppearance.DarkMode).filter(
key => typeof uiAppearance.DarkMode[key] === 'number');
async setFontScale(scale: number): Promise<void> {
let configInit: Configuration = {
fontSizeScale: scale,
};
abilityManager.updateConfiguration(configInit, (err: BusinessError) => {
if (err) {
hilog.error(DOMAIN, TAG, 'updateConfiguration fail, err: %{public}s', JSON.stringify(err));
this.getUIContext().getPromptAction().showToast({ message: `scale:${scale}, err:${JSON.stringify(err)}` });
} else {
this.currentFontSizeScale = String(scale);
if (scale > 1) {
this.fontSize = 8;
} else {
this.fontSize = 15;
}
hilog.info(DOMAIN, TAG, 'updateConfiguration success.');
this.getUIContext().getPromptAction().showToast({ message: `scale:${scale}, updateConfiguration success.` });
}
});
}
darkMode(isDarkMode: boolean): void {
let mode: uiAppearance.DarkMode = uiAppearance.DarkMode.ALWAYS_LIGHT;
if (isDarkMode) {
mode = uiAppearance.DarkMode.ALWAYS_DARK;
}
if (mode == uiAppearance.getDarkMode()) {
hilog.info(DOMAIN, TAG, `TitleDarkMode Set ${this.darkModeKey[mode]} successfully.`);
return;
}
try {
uiAppearance.setDarkMode(mode).then(() => {
hilog.info(DOMAIN, TAG, `TitleDarkMode Set ${this.darkModeKey[mode]} successfully.`);
}).catch((error: Error) => {
hilog.error(DOMAIN, TAG, `TitleDarkMode Set ${this.darkModeKey[mode]} failed, ${error.message}`);
});
} catch (error) {
let message = (error as BusinessError).message;
hilog.error(DOMAIN, TAG, `TitleDarkMode Set dark-mode failed, ${message}`);
}
}
build() {
NavDestination() {
Column() {
Column() {
Row() {
Text(`current fontSizeScale:${this.currentFontSizeScale}`)
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
}
Row() {
Button('1.75')
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
.width('40%')
.onClick(async () => {
await this.setFontScale(1.75);
})
Button('2')
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
.width('40%')
.onClick(async () => {
await this.setFontScale(2);
})
}.margin({ top: 25 })
Row() {
Button('3.2')
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
.width('40%')
.onClick(async () => {
await this.setFontScale(3.2);
})
Button('1')
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
.width('40%')
.onClick(async () => {
await this.setFontScale(1);
})
}
Row() {
// The value in the app.string.Dark_mode resource file is "Dark Mode."
Button($r('app.string.Dark_mode'))
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
.width('40%')
.onClick(async () => {
this.darkMode(true);
})
// The value in the app.string.Light_mode resource file is "Light Mode."
Button($r('app.string.Light_mode'))
.margin({ top: 5, bottom: 5 })
.fontSize(this.fontSize)
.width('40%')
.onClick(async () => {
this.darkMode(false);
})
}
}.alignItems(HorizontalAlign.Start)
Column() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Pink)
}.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'OverLength'))
TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Yellow)
}.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'SixLine'))
TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Blue)
}.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'Blue'))
TabContent() {
Column().width('100%').height('100%').backgroundColor(Color.Green)
}.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'Green'))
}
.vertical(false)
.scrollable(true)
.barMode(BarMode.Fixed)
.onChange((index: number) => {
hilog.info(DOMAIN, TAG, index.toString());
})
.width('100%')
.backgroundColor(0xF1F3F5)
}.width('80%').height(200)
.margin({ top: 200 })
}.width('100%')
}
}
}
Controlling the Number of Cached Pages
Since API version 19, you can use the cachedMaxCount API to set the maximum number of cached child components and specify the cache mode. By default, all TabContent components are preloaded when the Tabs component is created, and the loaded pages remain in memory, which may cause performance and memory issues. To control the number of cached pages, set the cachedMaxCount attribute. Once this attribute is configured, pages are no longer preloaded but instead use lazy loading (pages load only when switched to). When users switch between pages, the TabsCacheMode attribute determines whether to retain or release pages based on the specified mode.
NOTE
When TabsCacheMode is set to CACHE_BOTH_SIDE, the currently displayed child component and the adjacent components on both sides are cached.
When TabsCacheMode is set to CACHE_LATEST_SWITCHED, the currently displayed child component and the most recently switched child component are cached.
If a page switching animation is enabled and users switch directly from page 1 to page 3, the animation will include page 2, causing it to be temporarily loaded. If page 2 falls outside the cache range, it will be released immediately after the page switching animation completes.
Figure 15 Page switching by clicking the yellow button with page caching enabled

// To use it as the entry of a page, uncomment @Entry and delete the export keyword.
// @Entry
@Component
export struct NumberOfCachesTabBar {
build() {
// ...
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
MyComponent({ color: '#00CB87' })
}.tabBar(SubTabBarStyle.of('green'))
TabContent() {
MyComponent({ color: '#007DFF' })
}.tabBar(SubTabBarStyle.of('blue'))
TabContent() {
MyComponent({ color: '#FFBF00' })
}.tabBar(SubTabBarStyle.of('yellow'))
TabContent() {
MyComponent({ color: '#E67C92' })
}.tabBar(SubTabBarStyle.of('pink'))
TabContent() {
MyComponent({ color: '#FF0000' })
}.tabBar(SubTabBarStyle.of('red'))
}
.width(360)
.height(296)
.backgroundColor('#F1F3F5')
.cachedMaxCount(1, TabsCacheMode.CACHE_BOTH_SIDE)
// ...
}
}
@Component
struct MyComponent {
private color: string = '';
aboutToAppear(): void {
console.info('aboutToAppear backgroundColor:' + this.color);
}
aboutToDisappear(): void {
console.info('aboutToDisappear backgroundColor:' + this.color);
}
build() {
Column()
.width('100%')
.height('100%')
.backgroundColor(this.color)
}
}
-
In Figure 16, the default page switching animation is enabled and the CACHE_BOTH_SIDE mode is used with n=2. When users switch to the yellow page using the tab bar, TabContent1 through TabContent3 are cached. Subsequently switching to the red page releases TabContent1 and TabContent2, while caching TabContent3 through TabContent5.
Figure 16 Default page switching animation in CACHE_BOTH_SIDE mode

-
In Figure 17, the default page switching animation is enabled and the CACHE_LATEST_SWITCHED mode is used with n=2. When users switch to the yellow page using the tab bar, TabContent1 and TabContent3 are cached while TabContent2 is released. Subsequently switching to the red page caches TabContent1, TabContent3, and TabContent5, while releasing TabContent4.
Figure 17 Default page switching animation in CACHE_LATEST_SWITCHED mode

-
In Figure 18, the default page switching animation is disabled and the CACHE_BOTH_SIDE mode is used with n=2. When users switch to the yellow page using the tab bar, TabContent1 and TabContent3 are cached. Subsequently switching to the red page caches TabContent3 and TabContent5, while releasing TabContent1.
Figure 18 CACHE_BOTH_SIDE mode with the page switching animation disabled

-
In Figure 19, the default page switching animation is disabled and the CACHE_LATEST_SWITCHED mode is used with n=2. When users switch to the yellow page using the tab bar, TabContent1 and TabContent3 are cached. Subsequently switching to the red page caches TabContent1, TabContent3, and TabContent5.
Figure 19 CACHE_LATEST_SWITCHED mode with the page switching animation disabled
