Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/felipe-software/react-native-jelly-tabs/llms.txt

Use this file to discover all available pages before exploring further.

PillMaskedView is the low-level animated masking primitive that powers the selected-tab reveal effect inside JellyTabBarHeadless. It clips its children to a pill-shaped region that can be repositioned and resized via Reanimated animated styles.

Import

import { PillMaskedView } from 'react-native-jelly-tabs'

Overview

PillMaskedView is the low-level animated masking primitive that powers the selected-tab reveal effect inside JellyTabBarHeadless. It clips its children to a pill-shaped region that can be repositioned and resized via Reanimated animated styles.

Platform behavior

  • iOS / Android — uses @react-native-masked-view/masked-view with a hardware-rendered mask element. The mask is an absolutely-positioned Animated.View with borderRadius: 999, driven by animatedStyle.
  • Web — uses a CSS overflow: hidden clip box with border-radius (stable across Safari animation frames) driven by clipStyle for the pill shape, plus contentStyle for the inverse transform that keeps children fixed to the track coordinate system. This avoids an animated clip-path, which Safari drops on stray frames.

When to use

Use PillMaskedView when building a fully custom tab UI that needs the jelly pill masking primitive independently of the full JellyTabBarHeadless component. For most use-cases JellyTabBarHeadless already handles this internally.

Props

animatedStyle
AnimatedStyle<ViewStyle>
required
Reanimated animated style applied to the pill mask element on native platforms. This style drives the position and shape of the mask. Unused on web — see clipStyle instead.
clipStyle
AnimatedStyle<ViewStyle>
required
Reanimated animated style applied to the pill-shaped clip box on web. Typically contains a transform that repositions and scales the clip rect. The box’s border-radius is set from the height prop and stays stable across animation frames.
contentHeight
number
required
The total height of the content layer placed inside the clip box on web. Should equal trackHeight + maskOverscanY * 2 to cover the track plus its vertical overscan region.
contentStyle
AnimatedStyle<ViewStyle>
required
Reanimated animated style applied to the content container on web. This style carries the inverse transform of clipStyle, so children remain anchored to the track coordinate system while the clip box moves.
contentWidth
number
required
The total width of the content layer placed inside the clip box on web. Should equal trackWidth + maskOverscanX * 2 to cover the full track plus its horizontal overscan.
height
number
required
Pixel height of the pill-shaped mask region. Corresponds to the tab item height — typically config.layout.itemHeight * displayScale (default 56).
left
number
required
Pixel offset from the left edge of the mask overscan area to the starting position of the first pill. Typically maskOverscanX + trackInset.
tabWidth
number
required
Width of a single tab slot used as the initial width of the web clip box. Computed as (trackWidth - trackInset * 2) / tabCount.
top
number
required
Pixel offset from the top edge of the mask overscan area to the pill’s vertical position. Typically maskOverscanY + trackInset.
children
ReactNode
The content revealed through the pill mask. In JellyTabBarHeadless this is the selected-surface layer (background color + selectedBackdrop), the selected touch feedback glow, and the active-state tab icons/labels row.

Full example

import { useRef } from 'react'
import { View } from 'react-native'
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'
import { PillMaskedView } from 'react-native-jelly-tabs'

const TRACK_WIDTH = 360
const TAB_COUNT = 3
const TRACK_INSET = 4
const MASK_OVERSCAN_X = 48
const MASK_OVERSCAN_Y = 16
const ITEM_HEIGHT = 56
const TRACK_HEIGHT = 64
const TAB_WIDTH = (TRACK_WIDTH - TRACK_INSET * 2) / TAB_COUNT

export function CustomMaskedTabBar() {
  const pillX = useSharedValue(0)

  // Native mask: translate the pill shape
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: pillX.value }],
  }))

  // Web clip box: translate the clip rect
  const clipStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: pillX.value }],
  }))

  // Web content: inverse transform to keep content fixed
  const contentStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: -pillX.value }],
  }))

  return (
    <View style={{ width: TRACK_WIDTH, height: TRACK_HEIGHT, position: 'relative' }}>
      {/* overscan container */}
      <View
        style={{
          position: 'absolute',
          top: -MASK_OVERSCAN_Y,
          left: -MASK_OVERSCAN_X,
          right: -MASK_OVERSCAN_X,
          bottom: -MASK_OVERSCAN_Y,
        }}
      >
        <PillMaskedView
          animatedStyle={animatedStyle}
          clipStyle={clipStyle}
          contentHeight={TRACK_HEIGHT + MASK_OVERSCAN_Y * 2}
          contentStyle={contentStyle}
          contentWidth={TRACK_WIDTH + MASK_OVERSCAN_X * 2}
          height={ITEM_HEIGHT}
          left={MASK_OVERSCAN_X + TRACK_INSET}
          tabWidth={TAB_WIDTH}
          top={MASK_OVERSCAN_Y + TRACK_INSET}
        >
          {/* selected surface */}
          <View
            style={{
              position: 'absolute',
              top: MASK_OVERSCAN_Y + TRACK_INSET,
              left: MASK_OVERSCAN_X + TRACK_INSET,
              width: TAB_WIDTH,
              height: ITEM_HEIGHT,
              backgroundColor: '#f2eee7',
            }}
          />
        </PillMaskedView>
      </View>
    </View>
  )
}

Build docs developers (and LLMs) love