前言

你有没有注意过,很多 App 的顶部都有一排选项卡?比如新闻 App 的"推荐""热点""科技",电商 App 的"首页""分类""购物车"。这些选项卡有一个共同的特点:点一下某个标签,它就会高亮变色,同时其他标签恢复默认状态。这个交互叫"选项卡切换",是所有 UI 设计师工具箱里最基础的模式之一。
通常,实现选项卡需要用到复杂的容器或专门的控件。但在 HarmonyOS 里,有一个更轻巧的方式:就用普通的 Text 组件,加上 onClick 事件,配合 @State 状态管理,几行代码就能搭出一个漂亮的选项卡。不用 TabBar,不用额外的依赖,干净利落。上周我在 Pura X Max 模拟器上花了半小时做这件事,这篇文章就把整个过程完整记录下来------不止是代码,更想聊聊状态管理、条件渲染和"互斥选择"这个经典交互模式的实现原理。
一、选项卡的本质------一个整数状态和一组条件渲染
别看选项卡外表复杂,它在代码层面其实只做了一件事:维护一个整数变量,用来记录当前选中的是第几个标签。 这个整数可以是 0、1、2,分别代表第一个、第二个、第三个标签。当用户点击某个标签时,这个整数就更新为对应的索引值。界面上的所有样式变化,都是根据这个整数来判断的:如果当前选中的索引等于某个标签的索引,这个标签就显示"选中"样式(比如蓝色背景、白色文字);否则显示"未选中"样式(比如灰色背景、黑色文字)。

这个过程用 ArkUI 实现起来非常直观。我们定义一个 @State selectedIndex: number = 0,初始为 0,表示默认选中第一个标签。然后在模板里用 ForEach 遍历标签数组,根据 selectedIndex === index 来决定每个 Text 组件的样式。当用户点击某个 Text 时,在 onClick 回调里更新 selectedIndex 为这个标签的索引。
由于 @State 变量的变化会自动触发 UI 刷新,所以当 selectedIndex 更新后,所有 Text 的样式都会重新计算。原本高亮的旧标签会变回默认样式,被点击的新标签会获得高亮样式。这个切换是瞬间完成的,用户看到的就是"点了哪个,哪个就亮起来"。
这种"一个状态变量驱动多个子组件的条件样式"的模式,是声明式 UI 最核心的思维方式。你不需要写"把旧标签恢复原样,把新标签点亮"这种命令式代码,只需要描述"当选中索引等于 X 时,样式应该是 Y",框架会自动处理新旧状态的切换。
二、让标签看起来像标签------样式设计的细节
选项卡的视觉设计有几个原则:
- 选中状态要醒目:和未选中状态有足够对比度,通常用深色背景、亮色文字,或者加下划线、加粗。
- 未选中状态要低调:但也不能太暗淡,否则用户会以为它不可用。灰色文字、浅背景或透明背景即可。
- 标签之间要有界限感:要么用间距隔开,要么用分割线,要么每个标签是一个独立的圆角胶囊。
- 整体要紧凑:选项卡通常只有一行,标签不宜太多(3~6 个为宜),文字要简短。
在我们的 Demo 里,我选择了"胶囊"风格的选项卡:每个标签是一个独立的圆角矩形,未选中时灰色背景黑色文字,选中后变成蓝色背景白色文字,同时字体变粗。标签之间保持 8vp 的间距,整体放在一个 Row 里,用 padding 左右留白,看起来干净整齐。
这种样式可以用非常简洁的 ArkUI 属性实现:backgroundColor 和 fontColor 分别控制背景和文字颜色,borderRadius 控制圆角,padding 给标签内部留出呼吸空间,margin 控制标签之间的间距。所有属性都根据 selectedIndex === index 的条件来动态赋值。比如背景色:

backgroundColor: selectedIndex === index ? '#1976D2' : '#EEEEEE'
这种三元表达式在声明式 UI 里大量使用,简洁明了。
为了让交互更丰富,我还给每个标签添加了一个 onClick 回调,点击时除了更新 selectedIndex,还可以加入微小的动效。但在 ArkUI 中,要加动画需要用 animateTo 函数,稍微复杂一些。这个 Demo 里我们保持纯粹的点击切换,不加动画,体验已经很好。如果需要动画,可以用 animateTo({ duration: 200 }, () => { this.selectedIndex = index; }) 来实现背景色的平滑过渡。
三、选项内容也可以跟着变------不只是标签样式
选项卡不只是标签本身在变,通常它的下方内容区域也会跟着切换。比如新闻 App 里,点"热点"标签,下面展示热点新闻列表;点"科技",下面换成科技新闻列表。这就是"标签页"的完整形态。
实现内容区切换同样非常简单:在标签行下方,根据 selectedIndex 的值,用条件渲染显示不同的内容组件。比如:

if (this.selectedIndex === 0) {
Text('这是推荐内容').fontSize(16)
} else if (this.selectedIndex === 1) {
Text('这是热点内容').fontSize(16)
} else {
Text('这是科技内容').fontSize(16)
}
这样,用户点击标签时,不仅标签样式变了,下方内容也跟着刷新,形成一个完整的选项卡交互闭环。在 Demo 里,我加了一个简单的内容区,展示一个带背景色的卡片,上面写着当前选中的标签名称。这让整个 Demo 看起来更完整,不只是一排孤零零的文字。
四、完整代码------一个轻量级的选项卡组件
以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,把 entry/src/main/ets/pages/Index.ets 全部替换即可。无需任何权限,纯 UI 交互。

/*
* 点击文字变色选中 --- Text + onClick 选项卡效果
* 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
*/
@Entry
@Component
struct Index {
@State selectedIndex: number = 0; // 当前选中的标签索引
private tabs: string[] = ['推荐', '热点', '科技', '财经', '娱乐'];
build() {
Column() {
// 标题
Text('选项卡切换')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 30, bottom: 10 })
Text('点击标签切换内容与样式')
.fontSize(15)
.fontColor('#888')
.margin({ bottom: 20 })
// 标签行
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Text(tab)
.fontSize(16)
.fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.selectedIndex === index ? '#FFFFFF' : '#555555')
.backgroundColor(this.selectedIndex === index ? '#1976D2' : '#EEEEEE')
.borderRadius(20)
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.margin({ right: index === this.tabs.length - 1 ? 0 : 8 })
.onClick(() => {
this.selectedIndex = index;
})
})
}
.width('100%')
.padding({ left: 20, right: 20 })
.margin({ bottom: 25 })
// 内容区域
Column() {
Text(`📌 ${this.tabs[this.selectedIndex]} 内容区`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
.margin({ bottom: 8 })
Text(this.getContent(this.selectedIndex))
.fontSize(16)
.fontColor('#666')
.lineHeight(24)
.textAlign(TextAlign.Center)
}
.width('88%')
.height(200)
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 8, color: '#20000000', offsetY: 4 })
.justifyContent(FlexAlign.Center)
Text('💡 使用 @State 管理选中索引,点击切换标签样式和内容')
.fontSize(12)
.fontColor('#AAA')
.width('90%')
.textAlign(TextAlign.Center)
.margin({ top: 30 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 根据选中索引返回模拟内容
private getContent(index: number): string {
let contents = [
'这是推荐频道的内容,为你精选优质文章。',
'热点频道,实时追踪最新话题。',
'科技频道,探索前沿技术动态。',
'财经频道,把握市场脉搏。',
'娱乐频道,放松你的心情。'
];
return contents[index] || '';
}
}
代码分为三部分:标签数据定义、标签行渲染、内容区渲染。标签行用 ForEach 遍历 tabs 数组,每个 Text 根据 selectedIndex === index 切换样式。内容区用 getContent 方法返回不同文字,展示在不同标签下的内容变化。整个交互只依赖一个 @State 变量。
五、运行效果
把代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。屏幕上方是一行五个胶囊形状的标签,默认"推荐"标签蓝色高亮,其余灰色。点击"科技",蓝色高亮立刻转移到"科技"标签上,下方内容区同步变成"科技频道,探索前沿技术动态"。再点"娱乐",标签和内容再次切换。整个切换过程瞬间完成,标签样式变化干净利落,内容区卡片保持圆角阴影。手指依次点过五个标签,像在调收音机的频道,手感清脆,视觉反馈明确。


总结
这个小小的选项卡,虽然只有几十行代码,却把声明式 UI 开发的精髓展现得淋漓尽致:
- @State 状态驱动 :一个整数变量
selectedIndex是整个交互的核心,所有样式变化和内容切换都由它驱动。 - 条件渲染与动态样式:通过三元表达式和条件判断,让 UI 自动响应状态变化,无需手动操作 DOM。
- ForEach 简化重复组件:用数据数组遍历生成标签行,便于扩展。如果以后要加新标签,只需在数组里加一行即可,不用改模板。
- 组件复用思维 :同样是
Text组件,通过不同的属性配置,实现了完全不同的视觉效果和交互行为。
选项卡是移动端界面里最常见的模式之一,但理解它背后的原理后,你会发现它不过是一个整数状态加几行条件样式。这种"状态-视图"的映射关系,是声明式 UI 最迷人的地方。下次你在任何 App 里看到选项卡,也许会想:这背后,大概就是一个 selectedIndex 和几个三元表达式吧。