DEV Community

HarmonyOS
HarmonyOS

Posted on

Soft Edge-Fade Mask for Scrollable TabBar using overlay and linearGradient

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

  1. Build an overlay: Create an @Builder function (overlayBuilder) that returns two Stacks—one for the left fade, one for the right fade.
  2. Left fade: linearGradient({ direction: GradientDirection.Left, colors: [...] }) to fade into the Tabs background.
  3. Right fade: same idea with GradientDirection.Right.
  4. Pass-through touches: set .hitTestBehavior(HitTestMode.None) so the overlay doesn’t block scroll/press on the bar.
  5. Attach overlay: call .overlay(this.overlayBuilder()) on Tabs. Tune barHeight, 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%')
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

gif777.gif

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 barHeight changes, keep overlay heights in sync.
  • Performance: gradients are inexpensive; still avoid overly complex overlays.
  • Wearables: use smaller fades; see notes below.

Related Documents or Links

Written by Bunyamin Eymen Alagoz

Top comments (0)