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.

React Native Jelly Tabs is built with a dedicated semantic layer that sits on top of the gesture layer, giving VoiceOver (iOS) and TalkBack (Android) full visibility into each tab without interfering with the continuous drag animation.

Accessibility Architecture

The tab bar renders its visual content (the animated pill, icons, labels, and touch feedback) through a set of Animated and gesture-driven views. A completely separate, invisible accessibility layer is overlaid on top of these visual elements:
// Simplified representation of the accessibility layer
<View
  accessibilityRole="tab"
  accessibilityLabel={item.accessibilityLabel ?? item.label}
  accessibilityState={{ selected: semanticSelectedIndex === index }}
  accessibilityActions={actions}
  accessible
  pointerEvents="none"  // passes touches through to the gesture layer
  style={styles.accessibilityTab}
  testID={item.testID}
  onAccessibilityAction={handleAction}
/>
Because the accessibility views use pointerEvents="none", they are invisible to the pan gesture recognizer. VoiceOver and TalkBack interact with the semantic layer; drag gestures and swipe navigation interact with the gesture layer. The two systems operate without conflict.

accessibilityRole="tab"

Every tab in the bar is declared with accessibilityRole="tab". This tells VoiceOver and TalkBack to announce each element as a tab control, consistent with the ARIA tab role on the web. Screen readers typically announce the role as “tab” after the label (e.g., “Home, tab”).

Selected State

Each tab’s accessibility view carries accessibilityState={{ selected: true | false }}. The selected flag mirrors the pill’s current position:
  • The focused tab has selected: true — announced as “selected” by VoiceOver and TalkBack.
  • All other tabs have selected: false.
When selectedIndex is null or negative (no pill shown), all tabs report selected: false.

accessibilityLabel

Each accessible view uses item.accessibilityLabel if provided, or falls back to item.label. When building items for JellyTabBarHeadless, always set accessibilityLabel explicitly for the clearest announcements:
const items: TabsItem[] = [
  {
    key: "home",
    label: "Home",
    accessibilityLabel: "Home tab", // explicit label for screen readers
    activeIcon: HomeIconActive,
    inactiveIcon: HomeIconInactive,
  },
];
When using JellyTabBar with Expo Router or React Navigation, set tabBarAccessibilityLabel on the screen options — it maps to item.accessibilityLabel:
<Tabs.Screen
  name="home"
  options={{
    tabBarAccessibilityLabel: "Home tab",
    tabBarIcon: ({ color, size }) => (
      <MaterialIcons color={color} name="home" size={size} />
    ),
  }}
/>
Best practice: always provide an accessibilityLabel for every tab item. Without it, VoiceOver and TalkBack read the raw label string, which may be ambiguous without the “tab” suffix — especially when multiple word labels need contextual clarification.

Accessibility Actions

Activate

Every tab registers an activate accessibility action. When a VoiceOver or TalkBack user double-taps a focused element, the activate action fires and the tab bar treats it exactly like a tap — the pill animates to the tapped tab and onTabPress / onTabChange fire as usual.
// activate maps directly to tab press
onAccessibilityAction={(event) => {
  if (event.nativeEvent.actionName === "activate") {
    activateTab(index);
  }
}}
The activateTab function runs the full press pipeline, including any onTabPress rejection logic. If onTabPress returns false, the pill snaps back and the selection is not changed — even when triggered from a screen reader.

Long Press

When onTabLongPress is provided to JellyTabBarHeadless (or JellyTabBar when a screen registers a tabLongPress listener), the bar also registers a longpress accessibility action on every tab:
<JellyTabBarHeadless
  items={items}
  onTabLongPress={({ index, item }) => showContextMenu(item.key)}
/>
VoiceOver users can trigger the long press by choosing the Long Press custom action from the actions rotor. TalkBack users can activate it through the local context menu.
// longpress accessibility action handler (internal)
onAccessibilityAction={(event) => {
  if (event.nativeEvent.actionName === "longpress") {
    handleTabLongPress(index);
  }
}}
If onTabLongPress is not provided, the longpress action is not registered and the rotor entry does not appear.

testID

Each accessible tab view accepts a testID for automated testing with Detox, Maestro, or React Native Testing Library. Set it via TabsItem.testID when using JellyTabBarHeadless:
const items: TabsItem[] = [
  {
    key: "home",
    label: "Home",
    testID: "tab-home",
    activeIcon: HomeIconActive,
    inactiveIcon: HomeIconInactive,
  },
];
When using JellyTabBar with Expo Router or React Navigation, use the tabBarButtonTestID screen option — it maps to item.testID:
<Tabs.Screen
  name="home"
  options={{
    tabBarButtonTestID: "tab-home",
  }}
/>

Semantic Layer vs. Gesture Layer

A key design decision in Jelly Tabs is that the accessibility elements are separate from the gesture responders. The gesture layer is a GestureDetector wrapping the entire track, enabling smooth pan-to-drag behaviour. The accessibility layer sits above it with pointerEvents="none", so it never intercepts touch events from users who are not using a screen reader. This means:
  • Drag gestures work normally for sighted users because the gesture detector handles raw pointer events.
  • VoiceOver / TalkBack work correctly because screen readers interact with the accessible views, not the raw gesture surface.
  • There is no z-index conflict — the accessible views have zIndex: 3 and cover the full track, but their pointerEvents="none" setting means they are transparent to the gesture system.

Accessibility Checklist

ItemHow to configure
Label for each tabTabsItem.accessibilityLabel or tabBarAccessibilityLabel option
Selected stateAutomatic — driven by selectedIndex
Activate actionAutomatic — always registered
Long press actionAutomatic when onTabLongPress is provided
Test IDTabsItem.testID or tabBarButtonTestID option
Role announcementAutomatic — accessibilityRole="tab" on every tab

Build docs developers (and LLMs) love