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.

JellyTabBarHeadless is the router-agnostic core of the jelly tab bar. It accepts a plain array of TabsItem objects and exposes press and change callbacks, giving you full control over tab state independently of any navigation library.

Import

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

Overview

JellyTabBarHeadless is the router-agnostic core of the jelly tab bar. It accepts a plain array of TabsItem objects and exposes press/change callbacks, giving you full control over tab state independently of any navigation library. Use it when you need the jelly animation outside of React Navigation or Expo Router, or when you want to manage navigation state yourself.
JellyTabs is a deprecated alias for JellyTabBarHeadless and will be removed in a future major version. Migrate to the JellyTabBarHeadless name.

Sizing

JellyTabBarHeadless fills 100% of its parent’s width (up to maxWidth) and sets its own height internally. Wrap it in a container sized to:
height = config.layout.trackHeight × displayScale
The default trackHeight is 64 and the default displayScale is 1, giving a natural height of 64 dp.

Props

Items

items
readonly TabsItem[]
required
The list of tabs to render. Each item provides keys, labels, icons, badges, and accessibility metadata. The order determines the tab positions left-to-right.
interface TabsItem {
  key: string;                      // unique identifier
  label: string;                    // text shown below the icon
  labelStyle?: StyleProp<TextStyle>;
  activeIcon: TabsIcon;             // shown through the selected pill mask
  inactiveIcon: TabsIcon;           // shown in the unselected track
  badge?: number | string;
  badgeStyle?: StyleProp<TextStyle>;
  accessibilityLabel?: string;      // defaults to label
  testID?: string;
}
TabsIcon is a React component receiving { color, colors, size, opacity }.

Selection state

selectedIndex
number | null
Controlled selected-tab index. When provided, the component is fully controlled and the pill animates to the matching item on every change.
  • Pass null or a negative number to render no selected pill (e.g. while a modal screen is active).
  • Omit entirely to run in uncontrolled mode, where the component manages its own internal selection starting at index 0.

Interaction callbacks

onTabPress
(event: TabsChangeEvent) => boolean | void
Called after every completed tap or drag, including a press on the already-selected tab.
  • Return false to reject the selection change and spring the pill back to the current tab.
  • Return true or undefined to accept the change (the default).
interface TabsChangeEvent {
  index: number;
  item: TabsItem;
}
onTabChange
(event: TabsChangeEvent) => void
Called after a gesture accepts a tab change. Rejected presses and taps on the already-selected tab do not trigger this callback. Use this to synchronise external navigation state.
onTabLongPress
(event: TabsChangeEvent) => void
Called when a tab is long-pressed. Also triggered by the longpress accessibility action on the tab element. Only registers a long-press gesture when this prop is provided.

Colors & opacity

colors
Partial<TabBarColors>
Solid color overrides for the four visual layers. Partial objects are merged over the built-in defaults.
interface TabBarColors {
  surface: string;          // track background  — default: '#22211f'
  selectedSurface: string;  // selected pill      — default: '#f2eee7'
  activeContent: string;    // active icon/label  — default: '#11100f'
  inactiveContent: string;  // inactive icon/label — default: '#b8b4ad'
}
opacity
Partial<TabBarOpacity>
Per-layer opacity overrides, each clamped to [0, 1]. Opacity is applied to rendered content rather than the mask shape, keeping the pill clip fully opaque.
interface TabBarOpacity {
  surface: number;          // default: 1
  selectedSurface: number;  // default: 1
  activeContent: number;    // default: 1
  inactiveContent: number;  // default: 1
}

Layout & animation config

config
DeepPartial<TabBarConfig>
Deep-partial override of layout, jelly animation, and distortion parameters. Only the keys you provide are changed; everything else falls back to the defaults.
interface TabBarConfig {
  layout: {
    iconSize: number;       // default: 28
    itemHeight: number;     // default: 56
    trackHeight: number;    // default: 64
    trackInset: number;     // default: 4
    maskOverscanX: number;  // default: 48
    maskOverscanY: number;  // default: 16
  };
  pillJelly: {
    pressedScale: number;          // default: 1.3
    snapOnPointerDown: boolean;    // default: true
    frameConfig: {
      releaseDistanceFraction: number; // default: 0.025
      springs: Record<
        'panel' | 'press' | 'scaleX' | 'scaleY' | 'value' | 'velocity',
        { stiffness: number; dampingRatio: number }
      >;
    };
  };
  distortion: {
    pressedScale: number;  // default: 1.025
    touchFeedback: { opacity: number; middleOpacityRatio: number; radius: number; scale: number };
    spring: { damping: number; mass: number; stiffness: number };
    verticalDrag: { distortion: number; distanceForMaxDistortion: number; follow: number; rubberBand: number };
  };
}
maxWidth
DimensionValue
default:400
Maximum width of the tab bar track. The bar stays horizontally centered when the parent is wider. Accepts any React Native DimensionValue.
displayScale
number
default:1
Multiplier applied to every layout dimension. The container you wrap the component in should also be scaled: trackHeight × displayScale. Useful for recordings or adapting to density-independent layouts.

Backdrops

backdrop
ReactNode
A React node rendered below the track’s solid color layer. Use this to inject a blur view or custom background without tying the component to any specific blur library.
selectedBackdrop
ReactNode
A React node rendered below the selected-pill color layer. Use this to apply a blur or custom treatment exclusively inside the active pill.

Touch feedback

touchFeedbackEnabled
boolean
default:true
Enables or disables the radial glow rendered under the user’s finger during interaction. Set to false to remove the effect completely.
touchFeedbackColor
string
Overrides the color of the radial touch feedback. Defaults to colors.selectedSurface when not set.
touchFeedbackOpacity
number
Overrides the base opacity of the touch feedback glow. Defaults to config.distortion.touchFeedback.opacity (0.15).
touchFeedbackScale
number
Overrides the radius scale multiplier of the touch feedback glow. Defaults to config.distortion.touchFeedback.scale (2).

Misc

recording
boolean
default:false
Enables deterministic rendering mode. Disables non-deterministic animation behavior so you can capture clean screen recordings or automated visual snapshots.

Full example

import { useState } from 'react'
import { View, StyleSheet } from 'react-native'
import { JellyTabBarHeadless, type TabsItem, type TabsChangeEvent } from 'react-native-jelly-tabs'
import HomeIcon from './icons/HomeIcon'
import SearchIcon from './icons/SearchIcon'
import ProfileIcon from './icons/ProfileIcon'

const TABS: TabsItem[] = [
  {
    key: 'home',
    label: 'Home',
    activeIcon: ({ color, size }) => <HomeIcon color={color} size={size} />,
    inactiveIcon: ({ color, size }) => <HomeIcon color={color} size={size} />,
  },
  {
    key: 'search',
    label: 'Search',
    activeIcon: ({ color, size }) => <SearchIcon color={color} size={size} />,
    inactiveIcon: ({ color, size }) => <SearchIcon color={color} size={size} />,
  },
  {
    key: 'profile',
    label: 'Profile',
    activeIcon: ({ color, size }) => <ProfileIcon color={color} size={size} />,
    inactiveIcon: ({ color, size }) => <ProfileIcon color={color} size={size} />,
  },
]

export default function CustomTabBar() {
  const [selectedIndex, setSelectedIndex] = useState(0)

  const handleTabChange = (event: TabsChangeEvent) => {
    setSelectedIndex(event.index)
  }

  return (
    <View style={styles.container}>
      {/* trackHeight (64) × displayScale (1) */}
      <View style={{ height: 64, width: '100%' }}>
        <JellyTabBarHeadless
          items={TABS}
          selectedIndex={selectedIndex}
          onTabChange={handleTabChange}
          maxWidth={420}
          colors={{
            surface: '#1c1c1e',
            selectedSurface: '#f5f5f7',
            activeContent: '#1c1c1e',
            inactiveContent: '#8e8e93',
          }}
        />
      </View>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-end',
    paddingBottom: 32,
    paddingHorizontal: 20,
  },
})

Build docs developers (and LLMs) love