1# Using Tabs (Tabs)
2
3
4When 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](../reference/apis-arkui/arkui-ts/ts-container-tabs.md) 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.
5
6
7## Basic Layout
8
9  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.
10  **Figure 1** Tabs component layout
11
12![tabs-layout](figures/tabs-layout.png)
13
14
15>**NOTE**
16>
17> - 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.
18>
19> - 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.
20
21
22**Tabs** use braces to enclose the tab content, as shown in Figure 2.
23
24
25  **Figure 2** Using Tabs and TabContent
26
27![tabs-tabscontent](figures/tabs-tabscontent.png)
28
29
30Each **TabContent** component should be mapped to a tab page, which can be configured through the **tabBar** attribute. The following is an example.
31
32```ts
33 TabContent() {
34   Text('Home tab content').fontSize(30)
35 }
36.tabBar('Home')
37```
38
39
40When setting multiple **TabContent** components, place them in sequence in the **Tabs** component.
41
42```ts
43Tabs() {
44  TabContent() {
45    Text('Home tab content').fontSize(30)
46  }
47  .tabBar('Home')
48
49  TabContent() {
50    Text('Recommended tab content').fontSize(30)
51  }
52  .tabBar('Recommended')
53
54  TabContent() {
55    Text('Discover tab content').fontSize(30)
56  }
57  .tabBar('Discover')
58
59  TabContent() {
60    Text('Me tab content').fontSize(30)
61  }
62  .tabBar("Me")
63}
64```
65
66
67## Bottom Navigation
68
69Bottom 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.
70
71
72  **Figure 3** Bottom navigation bar
73
74![bottom-navigation](figures/bottom-navigation.gif)
75
76
77You set the position of the navigation bar through the **barPosition** parameter of the **Tabs** component. 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**.
78
79
80```ts
81Tabs({ barPosition: BarPosition.End }) {
82  // TabContent: Home, Discover, Recommended, and Me
83  ...
84}
85```
86
87
88## Top Navigation
89
90Top 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.
91
92  **Figure 4** Top navigation bar
93
94![top-navigation](figures/top-navigation.gif)
95
96
97```ts
98Tabs({ barPosition: BarPosition.Start }) {
99  // TabContent: Following, Video, Game, Digital, Technology, Sports, Movie
100  ...
101}
102```
103
104
105## Side Navigation
106
107Side 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.
108
109
110  **Figure 5** Side navigation bar
111
112![side-navigation](figures/side-navigation.png)
113
114
115To 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.
116
117
118
119```ts
120Tabs({ barPosition: BarPosition.Start }) {
121  // TabContent: Home, Discover, Recommended, and Me
122  ...
123}
124.vertical(true)
125.barWidth(100)
126.barHeight(200)
127```
128
129
130>**NOTE**
131>
132> - When the **vertical** attribute is set to **false**, the tab bar takes up the whole screen width by default. Set **barWidth** to a proper value.
133>
134> - When the **vertical** attribute is set to **true**, the tab bar takes up the actual content height by default. Set **barWidth** to a proper value.
135
136
137## Restricting the Scrolling of the Navigation Bar
138
139  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.
140  **Figure 6** Restricting the scrolling of the bottom navigation bar
141
142![restricted-navigation](figures/restricted-navigation.gif)
143
144
145The attribute that enables or disables the scrolling is **scrollable**. Its default value is **true**, indicating that scrolling is enabled. To disable the scrolling, set the attribute to **false**.
146
147```ts
148Tabs({ barPosition: BarPosition.End }) {
149  TabContent(){
150    Column(){
151      Tabs(){
152        // Content on the top navigation bar
153        ...
154      }
155    }
156    .backgroundColor('#ff08a8f1')
157    .width('100%')
158  }
159  .tabBar('Home')
160
161  // Other TabContent content: Discover, Recommended, and Me
162  ...
163}
164.scrollable(false)
165```
166
167
168## Fixed Navigation Bar
169
170When 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.
171
172
173  **Figure 7** Fixed navigation bar
174
175![fixed-navigation](figures/fixed-navigation.gif)
176
177
178To use a fixed navigation bar, set the **barMode** attribute of the **Tabs** component to **barMode.Fixed** (default).
179
180```ts
181Tabs({ barPosition: BarPosition.End }) {
182  // TabContent: Home, Discover, Recommended, and Me
183  ...
184}
185.barMode(BarMode.Fixed)
186```
187
188
189## Scrollable Navigation Bar
190
191The 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.
192
193
194  **Figure 8** Scrollable navigation bar
195
196![scrollable-navigation](figures/scrollable-navigation.gif)
197
198
199To use a scrollable navigation bar, set the **barMode** attribute of the **Tabs** component to **BarMode.Scrollable**.
200
201```ts
202Tabs({ barPosition: BarPosition.Start }) {
203  // TabContent: follow, video, game, digital, technology, sports, movie, humanities, art, nature, and military
204  ...
205}
206.barMode(BarMode.Scrollable)
207```
208
209
210## Customizing the Navigation Bar
211
212The 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.
213
214
215  **Figure 9** Custom navigation bar
216
217![custom-navigation-bar](figures/custom-navigation-bar.png)
218
219
220By 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.
221
222
223To 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).
224
225```ts
226@Builder tabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
227  Column() {
228    Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
229      .size({ width: 25, height: 25 })
230    Text(title)
231      .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
232  }
233  .width('100%')
234  .height(50)
235  .justifyContent(FlexAlign.Center)
236}
237```
238
239
240Pass the custom function component to the **tabBar** attribute corresponding to the tab content and transfer the corresponding parameters.
241
242```ts
243TabContent() {
244  Column(){
245    Text('Me tab content')
246  }
247  .width('100%')
248  .height('100%')
249  .backgroundColor('#007DFF')
250}
251.tabBar(this.tabBuilder('Me', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
252```
253
254
255## Switching to a Specified Tab
256
257Non-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.
258
259
260  **Figure 10** Content page and tab bar not synced
261
262![Content Page and Tab Bar Not Synced](figures/tabcontent_tabbar_not_sync.gif)
263
264To sync the content page with the tab bar, use the **onChange** event provided by **Tabs** to listen for changes in the index. Pass the currently active index value to **currentIndex** to enable tab switching.
265
266```ts
267@Entry
268@Component
269struct TabsExample1 {
270  @State currentIndex: number = 2
271
272  @Builder tabBuilder(title: string, targetIndex: number) {
273    Column() {
274      Text(title)
275        .fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
276    }
277  }
278
279  build() {
280    Column() {
281      Tabs({ barPosition: BarPosition.End }) {
282        TabContent() {
283          ...
284        }.tabBar(this.tabBuilder('Home', 0))
285
286        TabContent() {
287          ...
288        }.tabBar(this.tabBuilder('Discover', 1))
289
290        TabContent() {
291          ...
292        }.tabBar(this.tabBuilder('Recommended', 2))
293
294        TabContent() {
295          ...
296        }.tabBar(this.tabBuilder('Me',3))
297      }
298      .animationDuration(0)
299      .backgroundColor('#F1F3F5')
300      .onChange((index: number) => {
301        this.currentIndex = index
302      })
303    }.width('100%')
304  }
305}
306```
307  **Figure 11** Content page and tab bar synced
308
309![Content Page and Tab Bar Synced](figures/tabcontent_tabbar_sync.gif)
310
311To 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 content page switches. By using the **changeIndex** API of **TabsController**, you can set your application to display the tab content corresponding to the specified index.
312```ts
313@State currentIndex: number = 2
314private controller: TabsController = new TabsController()
315
316Tabs({ barPosition: BarPosition.End, index: this.currentIndex, controller: this.controller }) {
317  ...
318}
319.height(600)
320.onChange((index: number) => {
321   this.currentIndex = index
322})
323
324Button('Change Index').width('50%').margin({ top: 20 })
325  .onClick(()=>{
326    this.currentIndex = (this.currentIndex + 1) % 4
327})
328
329Button('changeIndex').width('50%').margin({ top: 20 })
330  .onClick(()=>{
331    let index = (this.currentIndex + 1) % 4
332    this.controller.changeIndex(index)
333})
334```
335
336  **Figure 12** Switching to a specific tab page
337
338![Switching to a Specified Tab Page](figures/TabsChange.gif)
339
340You can use the **onContentWillChange** API of the **Tabs** component to customize the interception callback function. 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.
341
342```ts
343Tabs({ barPosition: BarPosition.End, controller: this.controller, index: this.currentIndex }) {...}
344.onContentWillChange((currentIndex, comingIndex) => {
345  if (comingIndex == 2) {
346    return false
347  }
348  return true
349})
350```
351  **Figure 13** Customizing the page switching interception event
352
353![TabsChange3](figures/TabsChange3.gif)
354<!--Del-->
355## Supporting Aging-Friendly Design
356
357In 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.
358
359>  **NOTE**
360>
361> The dialog box applies only to bottom tab bars, that is, tab bars in the style of **BottomTabBarStyle**.
362
363**Figure 14** Displaying an aging-friendly dialog box by long-pressing the bottom tab bar in an aging-friendly scenario
364
365![Aging-Friendly Design](figures/tabs11.png)
366
367```ts
368import { abilityManager, Configuration } from '@kit.AbilityKit';
369import { BusinessError } from '@kit.BasicServicesKit';
370import { promptAction, uiAppearance } from '@kit.ArkUI';
371
372@Entry
373@Component
374struct Demo {
375  @State fontColor: string = '#182431';
376  @State selectedFontColor: string = '#007DFF';
377  @State currentIndex: number = 0;
378  @State currentFontSizeScale: string = '';
379  @State showBuilderTab: boolean = false;
380  @State fontSize: number = 15;
381  private darkModeKey: string[] = Object.keys(uiAppearance.DarkMode).filter(
382    key => typeof uiAppearance.DarkMode[key] === 'number')
383
384  async setFontScale(scale: number): Promise<void> {
385    let configInit: Configuration = {
386      fontSizeScale: scale,
387    };
388    abilityManager.updateConfiguration(configInit, (err: BusinessError) => {
389      if (err) {
390        console.error(`updateConfiguration fail, err: ${JSON.stringify(err)}`);
391        promptAction.showToast({ message: `scale:${scale}, err:${JSON.stringify(err)}` })
392      } else {
393        this.currentFontSizeScale = String(scale);
394        if (scale > 1) {
395          this.fontSize = 8;
396        } else {
397          this.fontSize = 15;
398        }
399        console.log('updateConfiguration success.');
400        promptAction.showToast({ message: `scale:${scale}, updateConfiguration success.` })
401      }
402    });
403  }
404
405  darkMode(isDarkMode: boolean): void {
406    let mode: uiAppearance.DarkMode = uiAppearance.DarkMode.ALWAYS_LIGHT;
407    if (isDarkMode) {
408      mode = uiAppearance.DarkMode.ALWAYS_DARK;
409    }
410    if (mode == uiAppearance.getDarkMode()) {
411      console.info(`TitleDarkMode Set ${this.darkModeKey[mode]} successfully.`)
412      return;
413    }
414    try {
415      uiAppearance.setDarkMode(mode).then(() => {
416        console.info(`TitleDarkMode Set ${this.darkModeKey[mode]} successfully.`)
417      }).catch((error: Error) => {
418        console.error(`TitleDarkMode Set ${this.darkModeKey[mode]} failed, ${error.message}`);
419      });
420    } catch (error) {
421      let message = (error as BusinessError).message;
422      console.error(`TitleDarkMode Set dark-mode failed, ${message}`);
423    }
424  }
425
426  build() {
427    Column() {
428      Column() {
429        Row() {
430          Text(`current fontSizeScale:${this.currentFontSizeScale}`)
431            .margin({ top: 5, bottom: 5 })
432            .fontSize(this.fontSize)
433        }
434
435        Row() {
436          Button('1.75')
437            .margin({ top: 5, bottom: 5 })
438            .fontSize(this.fontSize)
439            .width('40%')
440            .onClick(async () => {
441              await this.setFontScale(1.75);
442            })
443          Button('2')
444            .margin({ top: 5, bottom: 5 })
445            .fontSize(this.fontSize)
446            .width('40%')
447            .onClick(async () => {
448              await this.setFontScale(2);
449            })
450        }.margin({ top: 25 })
451
452        Row() {
453          Button('3.2')
454            .margin({ top: 5, bottom: 5 })
455            .fontSize(this.fontSize)
456            .width('40%')
457            .onClick(async () => {
458              await this.setFontScale(3.2);
459            })
460          Button('1')
461            .margin({ top: 5, bottom: 5 })
462            .fontSize(this.fontSize)
463            .width('40%')
464            .onClick(async () => {
465              await this.setFontScale(1);
466            })
467        }
468
469        Row() {
470          Button('Dark Mode')
471            .margin({ top: 5, bottom: 5 })
472            .fontSize(this.fontSize)
473            .width('40%')
474            .onClick(async () => {
475              this.darkMode(true);
476            })
477          Button('Light Mode')
478            .margin({ top: 5, bottom: 5 })
479            .fontSize(this.fontSize)
480            .width('40%')
481            .onClick(async () => {
482              this.darkMode(false);
483            })
484        }
485      }.alignItems(HorizontalAlign.Start)
486
487      Column() {
488        Tabs({ barPosition: BarPosition.End }) {
489          TabContent() {
490            Column().width('100%').height('100%').backgroundColor(Color.Pink)
491          }.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'OverLength'))
492          TabContent() {
493            Column().width('100%').height('100%').backgroundColor(Color.Yellow)
494          }.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'SixLine'))
495          TabContent() {
496            Column().width('100%').height('100%').backgroundColor(Color.Blue)
497          }.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'Blue'))
498          TabContent() {
499            Column().width('100%').height('100%').backgroundColor(Color.Green)
500          }.tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), 'Green'))
501        }
502        .vertical(false)
503        .scrollable(true)
504        .barMode(BarMode.Fixed)
505        .onChange((index: number) => {
506          console.info(index.toString())
507        })
508        .width('100%')
509        .backgroundColor(0xF1F3F5)
510      }.width('80%').height(200)
511      .margin({ top: 200 })
512    }.width('100%')
513  }
514}
515```
516<!--DelEnd-->