Beautiful, Web, Native and TV friendly interactable components and spatial navigation for React Native (iOS, Android, Web, TV) with accessibility for voice and screen reader support.
- Cross Platform Ready interactable UI: Buttons (native/custom), Switch, Dropdown, FlatLabelInput, Ripple, Portal.
- Spatial navigation primitives: Root, Focusable views, ScrollView, Virtualized List/Grid, hooks, and refs.
- Cross-platform pointer/remote support powered by @bam.tech/lrud for LRUD navigation and React Native Reanimated for silky animations.
- Installation
- Requirements
- Components
- Setup spatial navigation
- Spatial navigation overview
- Usage snippets
- API and types reference
- Components details
- Contributing and license
- Install the package and required peers
# with npm
npm i react-native-cross-elements
# or yarn
yarn add react-native-cross-elementsFollow the official Reanimated installation guide for your RN version:
- React Native Reanimated docs: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/
Typical steps include:
- Add 'react-native-reanimated/plugin' as the last plugin in babel.config.js.
- Enable Hermes (recommended).
- Rebuild the native app after installing.
After installation and Babel config, fully rebuild the app (npx pod-install && run).
- react-native
- react
- react-native-reanimated >= 3.0.0 (installation guide: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started#installation)
- Buttons
- Inputs
- Effects & Portal
- Navigation primitives
This setup is optional if you want to use spatial navigation (TV, remote, keyboard).
Otherwise, no need to wrap your app in a SpatialNavigationRoot.
Wrap your apps if you want to use spatial navigation (smart navigating with arrows button).
import React from 'react';
import { Text } from 'react-native';
import {
SpatialNavigationDeviceTypeProvider,
SpatialNavigationRoot,
SpatialNavigationFocusableView,
SpatialNavigationView,
SpatialNavigation,
BaseRemoteControl,
Directions,
} from 'react-native-cross-elements';
// Example: Custom remote control implementation
class MyRemoteControl extends BaseRemoteControl<string> {
constructor() {
super();
// Set up your platform-specific key listeners here
// For example, on web you might listen to keyboard events
if (typeof window !== 'undefined') {
window.addEventListener('keydown', this.handleKeyDown);
}
}
private handleKeyDown = (event: KeyboardEvent) => {
this.emitKeyDown(event.key);
};
}
const remoteControl = new MyRemoteControl();
export default function App() {
// Optional: configure keyboard/remote control once
React.useEffect(() => {
SpatialNavigation.configureRemoteControl({
mappedDirection: {
ArrowUp: Directions.UP,
ArrowDown: Directions.DOWN,
ArrowLeft: Directions.LEFT,
ArrowRight: Directions.RIGHT,
Enter: null, // null for select action
},
remoteControlSubscriber: (callback) => {
return remoteControl.addKeydownListener(callback);
},
remoteControlUnsubscriber: (subscriber) => {
remoteControl.removeKeydownListener(subscriber);
},
});
}, []);
return (
<SpatialNavigationDeviceTypeProvider>
<SpatialNavigationRoot>
<SpatialNavigationFocusableView style={{ padding: 12, backgroundColor: '#222', borderRadius: 8 }}>
<Text style={{ color: 'white' }}>Focusable card</Text>
</SpatialNavigationFocusableView>
</SpatialNavigationRoot>
</SpatialNavigationDeviceTypeProvider>
);
}- Engine: LRUD navigation is powered by @bam.tech/lrud.
- Root: SpatialNavigationRoot provides the navigation context and remote handling.
- Focusable: SpatialNavigationFocusableView turns a View into a focusable node with proper accessibility props.
- Views: SpatialNavigationView and SpatialNavigationScrollView help layout focusable children, with scrolling support.
- Virtualized: SpatialNavigationVirtualizedList/Grid expose focus and scroll APIs via refs.
- Events: onFocus, onBlur, onSelect, onLongSelect, onActive, onInactive handlers are available on focusable nodes.
More in-depth spatial navigation concepts:
- LRUD docs (BAM): https://github.com/bam-tech/lrud
- React TV Space Navigation (Bamlab): https://github.com/bamlab/react-tv-space-navigation
- See: BaseButtonProps
- See: PressableStyle
- See: AnimationConfig
- See: Ripple
import React from 'react';
import {
BaseButton,
NativeButton,
CustomButton,
ButtonSlider,
AutoDetectButtonsSlider,
} from 'react-native-cross-elements';
import {Text, View} from 'react-native';
export default function ButtonsShowcase() {
const [choice, setChoice] = React.useState(0);
return (
<View style={{gap: 16}}>
{/* BaseButton: full control with render-prop */}
<BaseButton
enableRipple
rippleDuration={350}
pressedScale={0.96}
backgroundColor="#111827"
selectedBackgroundColor="#1F2937"
pressedBackgroundColor="#0B1220"
textColor="#E5E7EB"
focusedTextColor="#FFFFFF"
animationConfig={{duration: 220}}
style={({focused, hovered, pressed}) => ([
{
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 12,
borderWidth: focused || hovered ? 2 : 1,
borderColor: focused ? '#60A5FA' : hovered ? '#93C5FD' : 'transparent',
opacity: pressed ? 0.92 : 1,
},
])}
onPress={() => console.log('BaseButton pressed')}
>
{({currentTextColor, isFocused}) => (
<Text style={{color: currentTextColor}}>
{isFocused ? 'Focused' : 'Not focused'} BaseButton
</Text>
)}
</BaseButton>
{/* NativeButton: text + optional icons + pending indicator */}
<NativeButton
text="Continue"
onPress={async () => new Promise(r => setTimeout(r, 500))}
showIndicator
leftIconComponent={(color) => <Text style={{color, marginRight: 8}}>➡️</Text>}
rightIconComponent={(color) => <Text style={{color, marginLeft: 8}}>⏩</Text>}
backgroundColor="#0F766E"
selectedBackgroundColor="#115E59"
pressedBackgroundColor="#0D4D4A"
textColor="#ECFDF5"
focusedTextColor="#FFFFFF"
style={{paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12}}
/>
{/* CustomButton: bring your own content with pending state */}
<CustomButton
onPress={async () => new Promise(r => setTimeout(r, 400))}
showIndicator
backgroundColor="#1D4ED8"
selectedBackgroundColor="#1E40AF"
pressedBackgroundColor="#1C3D99"
textColor="#DBEAFE"
focusedTextColor="#FFFFFF"
style={{paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12}}
>
{({currentTextColor}) => (
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Text style={{color: currentTextColor}}>Custom content</Text>
<Text style={{color: currentTextColor}}>🎨</Text>
</View>
)}
</CustomButton>
{/* ButtonSlider: fixed orientation */}
<ButtonSlider
options={["Low", "Medium", "High"]}
initialIndex={choice}
onSelect={(i) => setChoice(i)}
orientation="horizontal"
sliderContainerStyle={{backgroundColor: '#00000022', borderRadius: 9999, padding: 4}}
sliderStyle={{backgroundColor: '#111827'}}
sliderItemButtonStyle={({focused, isSelected}) => ({
backgroundColor: 'transparent',
})}
sliderItemTextStyle={({focused, isSelected}) => ({
color: isSelected ? '#FFFFFF' : '#111827',
fontWeight: focused ? '700' : '500',
})}
style={{width: 420, height: 44}}
/>
{/* AutoDetectButtonsSlider: auto horizontal/vertical based on container */}
<AutoDetectButtonsSlider
options={[
{label: "One", textProps: {numberOfLines: 1}},
{label: "Two", textProps: {numberOfLines: 1}},
{label: "Three", textProps: {numberOfLines: 1}},
{label: "Four", textProps: {numberOfLines: 1}}
]}
initialIndex={0}
onSelect={(i) => console.log('auto slider selected', i)}
sliderContainerStyle={{backgroundColor: '#00000022', borderRadius: 9999, padding: 4}}
sliderStyle={{backgroundColor: '#111827'}}
sliderItemButtonStyle={({focused, isSelected}) => ({
backgroundColor: isSelected ? '#11182720' : 'transparent'
})}
sliderItemTextStyle={({focused, isSelected}) => ({
color: isSelected ? '#FFFFFF' : '#111827',
fontWeight: isSelected ? '700' : '600'
})}
buttonClassName="slider-button"
textClassName="slider-text"
sliderRoundClassName="slider-round"
style={{width: 420, height: 44}}
/>
</View>
);
/>
</View>
)
;
}- See: DropdownProps
- See: DropdownRef
import React from 'react';
import { Text, View } from 'react-native';
import { Dropdown, type DropdownProps, type DropdownRef } from 'react-native-cross-elements';
const options = [
{ label: 'One', value: 1 },
{ label: 'Two', value: 2 },
{ label: 'Three', value: 3 },
{ label: 'Four', value: 4 },
];
export default function MyDropdown() {
const ref = React.useRef<DropdownRef>(null);
const onSelect: DropdownProps<(typeof options)[number]>['onSelect'] = (item, index) => {
console.log('selected', { item, index });
};
return (
<View style={{ gap: 12 }}>
<Dropdown
ref={ref}
data={options}
defaultValueByIndex={1}
disabledIndexes={[2]}
onSelect={onSelect}
onDropdownWillShow={(willShow) => console.log('dropdown will show?', willShow)}
// Animations
animateDropdown
animationType={'spring'}
animationConfig={{ duration: 280 }}
// Search
search
searchPlaceHolder="Search options..."
renderSearchInputLeftIcon={() => <Text>🔎</Text>}
// Window & overlay
dropDownSpacing={8}
dropdownOverlayColor="rgba(0,0,0,0.45)"
showsVerticalScrollIndicator={false}
// Custom UI
renderButtonContent={(selectedItem, isVisible, focused) => (
<View style={{ padding: 12, borderRadius: 8, backgroundColor: focused ? '#222' : '#333' }}>
<Text style={{ color: 'white' }}>
{selectedItem ? selectedItem.label : 'Select an option'} {isVisible ? '▲' : '▼'}
</Text>
</View>
)}
renderItemContent={(item, index, isSelected) => (
<View style={{ padding: 12, backgroundColor: isSelected ? '#222' : 'transparent' }}>
<Text style={{ color: 'white' }}>
{index + 1}. {item.label}
</Text>
</View>
)}
/>
<Text onPress={() => ref.current?.openDropdown()} style={{ color: '#4EA8DE' }}>
Open programmatically
</Text>
<Text onPress={() => ref.current?.selectIndex(0)} style={{ color: '#4EA8DE' }}>
Select first option
</Text>
</View>
);
}import React from 'react';
import { Switch } from 'react-native-cross-elements';
export default function MySwitch() {
const [on, setOn] = React.useState(false);
return <Switch value={on} onValueChange={setOn} />;
}Info: Web-optimized labeled input variant. Accepts the same InputConfig as FlatLabelInput and adds web-specific className styling hooks.
- See: InputConfig
- See: LabeledInputProps
- See: LabelInputState
- See: LabelInputStyle
- See: FlatInputProps
import React from 'react';
import { FlatLabelInput } from 'react-native-cross-elements';
import { Text } from 'react-native';
export default function MyInput() {
const [text, setText] = React.useState('');
const [focused, setFocused] = React.useState(false);
return (
<FlatLabelInput
onChange={setText}
// Visuals
backgroundColor="#111827"
selectedBackgroundColor="#1F2937"
pressedBackgroundColor="#0B1220"
labelStyle={{
labelFilledColor: '#9CA3AF',
labelFilledFontSize: 12,
color: '#9CA3AF',
fontSize: 16,
fontWeight: '600',
}}
textStyle={{
color: '#E5E7EB',
}}
inputConfig={{
placeholder: 'Email',
inputMode: 'email',
maxLength: 120,
autoFocus: false,
secureTextEntry: false,
onEndEditing: () => console.log('end editing'),
className: 'my-input',
placeholderClassName: 'my-input-placeholder',
}}
leftComponent={(state) => <Text style={{ marginRight: 8 }}>{state.focused ? '✉️' : '📧'}</Text>}
/>
);
}Use a PortalHost to render UI outside the normal view hierarchy. It's perfect for overlays that must escape clipping ( overflow: hidden) or stack above everything (modals, dropdowns, tooltips, toasts).
- PortalHost subscribes to a central registry and renders any mounted portals into an absolute, top-layer container ( zIndex 1000, pointerEvents: 'none').
- Portal registers its children into the named host on mount and removes them on unmount.
- Components like Dropdown auto-detect a PortalHost; if none is mounted, they fall back to a native modal.
import React from 'react';
import { View } from 'react-native';
import { PortalHost } from 'react-native-cross-elements';
export default function RootLayout() {
return (
<View style={{ flex: 1 }}>
{/* Top-level host. Name is optional; default is 'root_ui_portal'. */}
<PortalHost />
{/* Your app screens */}
{/* <AppNavigator /> */}
</View>
);
}import React from 'react';
import { Text, View } from 'react-native';
import { Portal } from 'react-native-cross-elements';
export function ToastDemo() {
const [toast, setToast] = React.useState<string | null>(null);
React.useEffect(() => {
const t = setInterval(() => setToast('Saved successfully ✅'), 5000);
const c = setInterval(() => setToast(null), 6500);
return () => {
clearInterval(t);
clearInterval(c);
};
}, []);
return (
<Portal>
{toast && (
<View
style={{
position: 'absolute',
bottom: 24,
left: 0,
right: 0,
alignItems: 'center',
// Important: enable interactions for overlays in the portal.
pointerEvents: 'auto',
}}
>
<View
style={{
paddingVertical: 10,
paddingHorizontal: 16,
borderRadius: 10,
backgroundColor: '#111827',
}}
>
<Text style={{ color: 'white' }}>{toast}</Text>
</View>
</View>
)}
</Portal>
);
}import React from 'react';
import { Text, View, Pressable } from 'react-native';
import { Portal } from 'react-native-cross-elements';
export function PopoverDemo() {
const [visible, setVisible] = React.useState(false);
return (
<View style={{ padding: 24 }}>
<Pressable onPress={() => setVisible((v) => !v)}>
<Text>Toggle popover</Text>
</Pressable>
<Portal>
{visible && (
<View style={{ position: 'absolute', top: 120, left: 24, pointerEvents: 'auto' }}>
<View style={{ padding: 8, backgroundColor: '#222', borderRadius: 8 }}>
<Text style={{ color: 'white' }}>I'm a popover</Text>
</View>
</View>
)}
</Portal>
</View>
);
}You can mount several hosts with different names and target them via the Portal's portalName.
// Root
<PortalHost name="top_layer"/>
<PortalHost name="hud"/>
// Later
<Portal portalName="hud">{/* Heads-up messages */}</Portal>- Interactivity: The host sets pointerEvents: 'none'. Give your top overlay container pointerEvents: 'auto' to receive touches/clicks.
- Stacking: Host uses zIndex 1000. You can stack additional layers inside using absolute positioning and zIndex.
- Fallbacks: Some components (e.g., Dropdown) use Portal when a host is mounted; otherwise they fall back to a modal.
Below are the key public types exported by the library. Use them for strong typing and better DX.
| Property | Type | Default | Description |
|---|---|---|---|
| duration | number | - | Duration of the animation in ms. |
| easing | EasingFunction | - | Easing used for the transition. |
| reduceMotion | ReduceMotion | - | Reduce motion for accessibility. |
- Either a style object for animated Pressable, or a function receiving a
PressableStateobject and returning the style. PressableStateincludes the default React Native pressable state plusfocusedandhovered.- Use it to render distinct focus, hover, and press visuals from a single callback.
type PressableState = PressableStateCallbackType & {
readonly focused: boolean;
readonly hovered: boolean;
};| Property | Type | Default | Description |
|---|---|---|---|
| orientation | 'horizontal' | 'vertical' | - | Orientation for spatial navigation. |
| onPress | (event: GestureResponderEvent) => any | - | Called when a single tap gesture is detected. |
| enableRipple | boolean | false | Enables ripple effect on press on native and web. |
| className | string | - | Optional classname for styling on web. |
| children | ReactNode | ((state: { currentTextColor: ColorValue | undefined; isFocused: boolean }) => ReactNode) |
required | Button content or render function with state. |
| pressedScale | number | - | Scale value when the button is pressed. |
| animationConfig | AnimationConfig | - | Animation configuration for button state transitions. |
| style | PressableStyle | - | Custom style for the button. Callback state exposes pressed, focused, and hovered. |
| textColor | ColorValue | 'black' | Text color when not focused. |
| focusedTextColor | ColorValue | 'black' | Text color when focused or hovered. |
| backgroundColor | ColorValue | 'white' | Button background color for the default state. |
| selectedBackgroundColor | ColorValue | 'white' | Background color when the button is focused or hovered. |
| pressedBackgroundColor | ColorValue | 'white' | Background color when the button is pressed. |
| rippleColor | ColorValue | - | Ripple color for the button press effect. |
| centerRipple | boolean | false | If true, the ripple starts at the center of the button. |
| rippleDuration | number | - | Duration of the ripple animation in milliseconds. |
| ...PressableProps | Omit<PressableProps, 'onPress' | 'children' | 'style' | 'className'> | - | All other React Native Pressable props. |
| Property | Type | Default | Description |
|---|---|---|---|
| All LabeledInputProps except labelStyle | - | - | Inherits all labeled input props except labelStyle. |
| labelStyle | { labelFilledFontSize?, labelFilledColor?, ...TextStyle } |
- | Label style and filled state props. |
| inputStyle | ViewStyle (partial) | - | Style for the input view component. |
| Property | Type | Default | Description |
|---|---|---|---|
| onChange | (text: string) => void | - | Called when the input text changes. |
| style | LabelInputStyle | (state: LabelInputState) => LabelInputStyle | - | Container style for layout properties. |
| labelStyle | { labelFilledOffset?, labelFilledFontSize?, labelFilledColor?, ...TextStyle } |
- | Label style and filled state props. |
| textStyle | TextStyle | - | Typography for label and placeholder text. |
| className | string | - | Container CSS class on web. |
| inputConfig | InputConfig | required | Native TextInput props plus web classes. |
| leftComponent | ReactElement | (state: LabelInputState) => ReactElement | - | Optional leading icon. |
| rightComponent | ReactElement | (state: LabelInputState) => ReactElement | - | Optional trailing icon. |
| backgroundColor | ColorValue | - | Background color. |
| selectedBackgroundColor | ColorValue | - | Background when selected. |
| pressedBackgroundColor | ColorValue | - | Background when pressed. |
| Property | Type | Description |
|---|---|---|
| className | string | CSS class for the input on web. |
| placeholderClassName | string | CSS class for the placeholder on web. |
| ...TextInputProps | All standard React Native TextInput props except style, onFocus, onBlur, onPointerEnter, onPointerLeave, onChangeText | Pass-through native input props. |
| Property | Type | Default | Description |
|---|---|---|---|
| data | T[] | required | Items to render in the dropdown. |
| onSelect | (item: T, index: number) => void | - | Called on item selection. |
| onDropdownWillShow | (willShow: boolean) => void | - | Called before opening or closing. |
| defaultValue | T | - | Pre-selected value. |
| defaultValueByIndex | number | - | Pre-selected index, zero-based. |
| disabled | boolean | false | Disable the entire dropdown. |
| disabledIndexes | number[] | - | Disable specific rows. |
| disableAutoScroll | boolean | false | Prevent auto scroll to selection. |
| testID | string | - | Test id for the list. |
| onFocus / onBlur | () => void | - | Focus lifecycle callbacks. |
| onScrollEndReached | () => void | - | Fired at the end of the list. |
| onChangeSearchInputText | (text: string) => void | - | Use your own search handler and disable internal filtering. |
| dropDownSpacing | number | - | Space between the trigger button and the dropdown window. |
| dropdownStyle | ViewStyle | - | Container style. |
| statusBarTranslucent | boolean | - | Show under the Android status bar. |
| dropdownOverlayColor | string | - | Backdrop color. |
| showsVerticalScrollIndicator | boolean | - | Show the vertical scroll bar. |
| animateDropdown | boolean | - | Enable opening and closing animation. |
| animationConfig | AnimationConfig | - | Timing config when using timing animation. |
| springConfig | WithSpringConfig | - | Spring config when using spring animation. |
| animationType | 'spring' | 'timing' | 'spring' | Choose the animation driver. |
| search | boolean | - | Enable the built-in search input. |
| searchInputStyle | ViewStyle | - | Search container style. |
| searchInputTxtColor | string | - | Search input text color. |
| searchInputTxtStyle | ViewStyle | - | Search input text style. |
| searchPlaceHolder | string | - | Search placeholder text. |
| searchPlaceHolderColor | string | - | Search placeholder color. |
| renderSearchInputLeftIcon | () => ReactElement | - | Left icon renderer. |
| renderSearchInputRightIcon | () => ReactElement | - | Right icon renderer. |
| renderButton | ({ selectedItem, isVisible, disabled, onPress }) => JSX.Element | - | Custom trigger button. |
| renderButtonContent | (selectedItem, isVisible, focused) => JSX.Element | - | Custom content inside the trigger. |
| renderItemButton | ({ item, index, isSelected, disabled, onPress }) => JSX.Element | - | Custom item button. |
| renderItemContent | (item, index, isSelected) => JSX.Element | - | Custom item content. |
| Method | Signature | Description |
|---|---|---|
| reset | () => void | Clear selection and search. |
| openDropdown | () => void | Open programmatically. |
| closeDropdown | () => void | Close programmatically. |
| selectIndex | (index: number) => void | Select item by index. |
Spatial navigation types and component APIs now live in SPATIAL_NAVIGATION_API.md.
- Types:
FocusableViewProps,SpatialNavigationNodeDefaultProps,SpatialNavigationNodeRef,SpatialNavigationVirtualizedListRef,CustomScrollViewProps,NodeOrientation,TypeVirtualizedListAnimation - Components:
SpatialNavigationRoot,SpatialNavigationView,SpatialNavigationScrollView,SpatialNavigationFocusableView,SpatialNavigationNode,SpatialNavigationVirtualizedList,SpatialNavigationVirtualizedGrid,DefaultFocus,SpatialNavigationDeviceTypeProvider
Visual press feedback effect available in BaseButton and other interactables. Enable via enableRipple and configure color/duration.
Container that participates in spatial (D‑Pad) navigation when a SpatialNavigationRoot is present. Falls back to a plain View otherwise.
Full props and usage notes: SPATIAL_NAVIGATION_API.md#spatialnavigationview
ScrollView that keeps the focused child in view when navigating with a remote/keyboard, with optional hover arrows for pointer devices.
Full props and usage notes: SPATIAL_NAVIGATION_API.md#spatialnavigationscrollview
Focusable wrapper that renders a View and exposes node state to children. See FocusableViewProps for the full API.
Top-level provider that enables spatial navigation, remote handling, and focus management.
Full props and usage notes: SPATIAL_NAVIGATION_API.md#spatialnavigationroot
Low-level focusable node used internally by SpatialNavigationFocusableView. Exposes focus lifecycle events and can be referenced via SpatialNavigationNodeRef.
Virtualized list integrated with spatial navigation. Provides focus(index) and scrollTo(index) via ref.
Virtualized grid version exposing the same ref API as the list.
Marks a node as initially focused within a subtree when the root activates.
Provider that detects device type (pointer/remote) and adapts focus interactions accordingly.
PRs and issues are welcome. See LICENSE for details (MIT).
Author: ImRoodyDev (https://github.com/imroodydev)
