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.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.
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: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 carriesaccessibilityState={{ 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.
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:
JellyTabBar with Expo Router or React Navigation, set tabBarAccessibilityLabel on the screen options — it maps to item.accessibilityLabel:
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 anactivate 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.
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
WhenonTabLongPress is provided to JellyTabBarHeadless (or JellyTabBar when a screen registers a tabLongPress listener), the bar also registers a longpress accessibility action on every tab:
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:
JellyTabBar with Expo Router or React Navigation, use the tabBarButtonTestID screen option — it maps to item.testID:
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 aGestureDetector 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: 3and cover the full track, but theirpointerEvents="none"setting means they are transparent to the gesture system.
Accessibility Checklist
| Item | How to configure |
|---|---|
| Label for each tab | TabsItem.accessibilityLabel or tabBarAccessibilityLabel option |
| Selected state | Automatic — driven by selectedIndex |
| Activate action | Automatic — always registered |
| Long press action | Automatic when onTabLongPress is provided |
| Test ID | TabsItem.testID or tabBarButtonTestID option |
| Role announcement | Automatic — accessibilityRole="tab" on every tab |