Read the original article:Soft Edge-Fade Mask for Scrollable TabBar using overlay and linearGradient
Soft Edge-Fade Mask for Scrollable TabBar using overlay and linearGradient
Requirement Description
When a Tabs bar has many items and scrolls horizontally, simply giving the Tabs a solid backgroundColor is visually harsh. We need a soft edge-fade (mask) on the left and right of the tabBar so users understand there’s more content off-screen, improving perceived usability.
Background Knowledge
- Tabs: container with a tab bar and pages.
- overlay(universal attribute): add floating content (text or custom component) above a component.
- linearGradient (universal attribute): gradient fills; we’ll use it to draw the left/right fades.
Implementation Steps
-
Build an overlay: Create an
@Builderfunction (overlayBuilder) that returns twoStacks—one for the left fade, one for the right fade. -
Left fade:
linearGradient({ direction: GradientDirection.Left, colors: [...] })to fade into the Tabs background. -
Right fade: same idea with
GradientDirection.Right. -
Pass-through touches: set
.hitTestBehavior(HitTestMode.None)so the overlay doesn’t block scroll/press on the bar. -
Attach overlay: call
.overlay(this.overlayBuilder())onTabs. TunebarHeight, gradient opacity/width, and colors to match your theme.
Code Snippet / Configuration
@Entry
@Component
struct TabsExample {
@State fontColor: string = '#ff3b3838'
@State selectedFontColor: string = '#007DFF'
@State currentIndex: number = 0
@State selectedIndex: number = 0
private controller: TabsController = new TabsController()
@Builder
tabBuilder(index: number, name: string) {
Column() {
Text(name)
.fontColor(this.selectedIndex === index ? this.selectedFontColor : this.fontColor)
.fontSize(12)
.fontWeight(this.selectedIndex === index ? 500 : 400)
.lineHeight(16)
.margin({ top: 8, bottom: 4 })
Divider()
.strokeWidth(2)
.color('#007DFF')
.opacity(this.selectedIndex === index ? 1 : 0)
}.width('27%')
.borderColor('#ffffffff')
.backgroundColor(Color.White)
}
@Builder
overlayBuilder() {
Row() {
Stack()
.height('100%')
.width('50%')
.linearGradient({
direction: GradientDirection.Right,
colors: [['#ff050505', 0.0], ['#4a050505', 0.6]]
})
.hitTestBehavior(HitTestMode.None)
.height(60)
Stack()
.height('100%')
.width('50%')
.linearGradient({
direction: GradientDirection.Left,
colors: [['#ff050505', 0.0], ['#4a050505', 0.6]]
})
.hitTestBehavior(HitTestMode.None)
.height(60)
}
.height(60)
.hitTestBehavior(HitTestMode.None)
}
build() {
Column() {
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.controller }) {
TabContent() {
Column().width('100%').height('100%').backgroundColor('#00CB87')
}
.tabBar(this.tabBuilder(0, 'Trending'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#007DFF')
}
.tabBar(this.tabBuilder(1, 'TV Shows'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#FFBF00')
}
.tabBar(this.tabBuilder(2, 'Movies'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#E67C92')
}
.tabBar(this.tabBuilder(3, 'Shorts'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#00CB87')
}
.tabBar(this.tabBuilder(4, 'Variety'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#007DFF')
}
.tabBar(this.tabBuilder(5, 'Anime'))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#FFBF00')
}
.tabBar(this.tabBuilder(6, 'Docs'))
}
.overlay(this.overlayBuilder())
.vertical(false)
.barMode(BarMode.Scrollable)
.barWidth('100%')
.barHeight(60)
.animationDuration(400)
.onChange((index: number) => {
this.currentIndex = index
this.selectedIndex = index
})
.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
if (index !== targetIndex) {
this.selectedIndex = targetIndex
}
})
.width('100%')
.height('100%')
.backgroundColor(Color.White)
// .backgroundColor('#ff050505')
}.width('100%')
}
}
Test Results
- Left and right edge-fade masks render above the tabBar and do not intercept gestures.
- Long tab lists remain scrollable with clear affordance that content continues off-screen.
Limitations or Considerations
- Theme/contrast: adjust gradient colors to your app’s bar background (light/dark).
- RTL locales: you may want to mirror or tweak fade strengths.
-
Dynamic sizes: if
barHeightchanges, keep overlay heights in sync. - Performance: gradients are inexpensive; still avoid overly complex overlays.
- Wearables: use smaller fades; see notes below.

Top comments (0)