Skip to content

Repository files navigation

# react-native-cross-elements

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.

npm versionnpm downloadsTypeScriptReanimated


✨ Features

  • 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.

🗂️ Table of contents

  • Installation
  • Requirements
  • Components
  • Setup spatial navigation
  • Spatial navigation overview
  • Usage snippets
  • API and types reference
  • Components details
  • Contributing and license

📦 Installation

  1. Install the package and required peers
# with npm
npm i react-native-cross-elements
# or yarn
yarn add react-native-cross-elements

2) Configure Reanimated (v3.0+)

Follow the official Reanimated installation guide for your RN version:

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.

3) iOS/Android native rebuild

After installation and Babel config, fully rebuild the app (npx pod-install && run).

⚙️ Requirements

🧩 Components

⚡ Setup Spatial Navigation

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).

importReactfrom'react';import{Text}from'react-native';import{SpatialNavigationDeviceTypeProvider,SpatialNavigationRoot,SpatialNavigationFocusableView,SpatialNavigationView,SpatialNavigation,BaseRemoteControl,Directions,}from'react-native-cross-elements';// Example: Custom remote control implementationclassMyRemoteControlextendsBaseRemoteControl<string>{constructor(){super();// Set up your platform-specific key listeners here// For example, on web you might listen to keyboard eventsif(typeofwindow!=='undefined'){window.addEventListener('keydown',this.handleKeyDown);}}privatehandleKeyDown=(event: KeyboardEvent)=>{this.emitKeyDown(event.key);};}constremoteControl=newMyRemoteControl();exportdefaultfunctionApp(){// Optional: configure keyboard/remote control onceReact.useEffect(()=>{SpatialNavigation.configureRemoteControl({mappedDirection: {ArrowUp: Directions.UP,ArrowDown: Directions.DOWN,ArrowLeft: Directions.LEFT,ArrowRight: Directions.RIGHT,Enter: null,// null for select action},remoteControlSubscriber: (callback)=>{returnremoteControl.addKeydownListener(callback);},remoteControlUnsubscriber: (subscriber)=>{remoteControl.removeKeydownListener(subscriber);},});},[]);return(<SpatialNavigationDeviceTypeProvider><SpatialNavigationRoot><SpatialNavigationFocusableViewstyle={{padding: 12,backgroundColor: '#222',borderRadius: 8}}><Textstyle={{color: 'white'}}>Focusable card</Text></SpatialNavigationFocusableView></SpatialNavigationRoot></SpatialNavigationDeviceTypeProvider>);}

🧭 Spatial navigation

  • 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:

🧪 Usage snippets

Buttons (Base, Native, Custom, Sliders)

BaseButton

NativeButton

CustomButton

ButtonsSlider

AutoDetectButtonsSlider

importReactfrom'react';import{BaseButton,NativeButton,CustomButton,ButtonSlider,AutoDetectButtonsSlider,}from'react-native-cross-elements';import{Text,View}from'react-native';exportdefaultfunctionButtonsShowcase(){const[choice,setChoice]=React.useState(0);return(<Viewstyle={{gap: 16}}>{/* BaseButton: full control with render-prop */}<BaseButtonenableRipplerippleDuration={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})=>(<Textstyle={{color: currentTextColor}}>{isFocused ? 'Focused' : 'Not focused'} BaseButton
</Text>)}</BaseButton>{/* NativeButton: text + optional icons + pending indicator */}<NativeButtontext="Continue"onPress={async()=>newPromise(r=>setTimeout(r,500))}showIndicatorleftIconComponent={(color)=><Textstyle={{color,marginRight: 8}}>➡️</Text>}rightIconComponent={(color)=><Textstyle={{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 */}<CustomButtononPress={async()=>newPromise(r=>setTimeout(r,400))}showIndicatorbackgroundColor="#1D4ED8"selectedBackgroundColor="#1E40AF"pressedBackgroundColor="#1C3D99"textColor="#DBEAFE"focusedTextColor="#FFFFFF"style={{paddingHorizontal: 16,paddingVertical: 12,borderRadius: 12}}>{({currentTextColor})=>(<Viewstyle={{flexDirection: 'row',alignItems: 'center',gap: 8}}><Textstyle={{color: currentTextColor}}>Custom content</Text><Textstyle={{color: currentTextColor}}>🎨</Text></View>)}</CustomButton>{/* ButtonSlider: fixed orientation */}<ButtonSlideroptions={["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 */}<AutoDetectButtonsSlideroptions={[{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>);}

Dropdown

importReactfrom'react';import{Text,View}from'react-native';import{Dropdown,typeDropdownProps,typeDropdownRef}from'react-native-cross-elements';constoptions=[{label: 'One',value: 1},{label: 'Two',value: 2},{label: 'Three',value: 3},{label: 'Four',value: 4},];exportdefaultfunctionMyDropdown(){constref=React.useRef<DropdownRef>(null);constonSelect: DropdownProps<(typeofoptions)[number]>['onSelect']=(item,index)=>{console.log('selected',{ item, index });};return(<Viewstyle={{gap: 12}}><Dropdownref={ref}data={options}defaultValueByIndex={1}disabledIndexes={[2]}onSelect={onSelect}onDropdownWillShow={(willShow)=>console.log('dropdown will show?',willShow)}// AnimationsanimateDropdownanimationType={'spring'}animationConfig={{duration: 280}}// SearchsearchsearchPlaceHolder="Search options..."renderSearchInputLeftIcon={()=><Text>🔎</Text>}// Window & overlaydropDownSpacing={8}dropdownOverlayColor="rgba(0,0,0,0.45)"showsVerticalScrollIndicator={false}// Custom UIrenderButtonContent={(selectedItem,isVisible,focused)=>(<Viewstyle={{padding: 12,borderRadius: 8,backgroundColor: focused ? '#222' : '#333'}}><Textstyle={{color: 'white'}}>{selectedItem ? selectedItem.label : 'Select an option'}{isVisible ? '▲' : '▼'}</Text></View>)}renderItemContent={(item,index,isSelected)=>(<Viewstyle={{padding: 12,backgroundColor: isSelected ? '#222' : 'transparent'}}><Textstyle={{color: 'white'}}>{index+1}. {item.label}</Text></View>)}/><TextonPress={()=>ref.current?.openDropdown()}style={{color: '#4EA8DE'}}>
Open programmatically
</Text><TextonPress={()=>ref.current?.selectIndex(0)}style={{color: '#4EA8DE'}}>
Select first option
</Text></View>);}

Switch

importReactfrom'react';import{Switch}from'react-native-cross-elements';exportdefaultfunctionMySwitch(){const[on,setOn]=React.useState(false);return<Switchvalue={on}onValueChange={setOn}/>;}

FlatLabelInput, LabeledInputField, LabeledInputFieldWeb

Info: Web-optimized labeled input variant. Accepts the same InputConfig as FlatLabelInput and adds web-specific className styling hooks.

importReactfrom'react';import{FlatLabelInput}from'react-native-cross-elements';import{Text}from'react-native';exportdefaultfunctionMyInput(){const[text,setText]=React.useState('');const[focused,setFocused]=React.useState(false);return(<FlatLabelInputonChange={setText}// VisualsbackgroundColor="#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)=><Textstyle={{marginRight: 8}}>{state.focused ? '✉️' : '📧'}</Text>}/>);}

Portal & PortalHost

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).

How it works

  • 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.

Setup (root)

importReactfrom'react';import{View}from'react-native';import{PortalHost}from'react-native-cross-elements';exportdefaultfunctionRootLayout(){return(<Viewstyle={{flex: 1}}>{/* Top-level host. Name is optional; default is 'root_ui_portal'. */}<PortalHost/>{/* Your app screens */}{/* <AppNavigator /> */}</View>);}

Example: global toast

importReactfrom'react';import{Text,View}from'react-native';import{Portal}from'react-native-cross-elements';exportfunctionToastDemo(){const[toast,setToast]=React.useState<string|null>(null);React.useEffect(()=>{constt=setInterval(()=>setToast('Saved successfully ✅'),5000);constc=setInterval(()=>setToast(null),6500);return()=>{clearInterval(t);clearInterval(c);};},[]);return(<Portal>{toast&&(<Viewstyle={{position: 'absolute',bottom: 24,left: 0,right: 0,alignItems: 'center',// Important: enable interactions for overlays in the portal.pointerEvents: 'auto',}}><Viewstyle={{paddingVertical: 10,paddingHorizontal: 16,borderRadius: 10,backgroundColor: '#111827',}}><Textstyle={{color: 'white'}}>{toast}</Text></View></View>)}</Portal>);}

Example: anchored overlay/popover

importReactfrom'react';import{Text,View,Pressable}from'react-native';import{Portal}from'react-native-cross-elements';exportfunctionPopoverDemo(){const[visible,setVisible]=React.useState(false);return(<Viewstyle={{padding: 24}}><PressableonPress={()=>setVisible((v)=>!v)}><Text>Toggle popover</Text></Pressable><Portal>{visible&&(<Viewstyle={{position: 'absolute',top: 120,left: 24,pointerEvents: 'auto'}}><Viewstyle={{padding: 8,backgroundColor: '#222',borderRadius: 8}}><Textstyle={{color: 'white'}}>I'm a popover</Text></View></View>)}</Portal></View>);}

Multiple hosts

You can mount several hosts with different names and target them via the Portal's portalName.

// Root<PortalHostname="top_layer"/><PortalHostname="hud"/>// Later<PortalportalName="hud">{/* Heads-up messages */}</Portal>

Notes

  • 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.

📚 API and types reference

Below are the key public types exported by the library. Use them for strong typing and better DX.

Interactables types

AnimationConfig (for Switch, Dropdown, etc.)

PropertyTypeDefaultDescription
durationnumber-Duration of the animation in ms.
easingEasingFunction-Easing used for the transition.
reduceMotionReduceMotion-Reduce motion for accessibility.

PressableStyle

  • Either a style object for animated Pressable, or a function receiving a PressableState object and returning the style.
  • PressableState includes the default React Native pressable state plus focused and hovered.
  • Use it to render distinct focus, hover, and press visuals from a single callback.
typePressableState=PressableStateCallbackType&{readonlyfocused: boolean;readonlyhovered: boolean;};

BaseButtonProps

PropertyTypeDefaultDescription
orientation'horizontal' | 'vertical'-Orientation for spatial navigation.
onPress(event: GestureResponderEvent) => any-Called when a single tap gesture is detected.
enableRipplebooleanfalseEnables ripple effect on press on native and web.
classNamestring-Optional classname for styling on web.
childrenReactNode | ((state: { currentTextColor: ColorValue | undefined; isFocused: boolean }) => ReactNode)requiredButton content or render function with state.
pressedScalenumber-Scale value when the button is pressed.
animationConfigAnimationConfig-Animation configuration for button state transitions.
stylePressableStyle-Custom style for the button. Callback state exposes pressed, focused, and hovered.
textColorColorValue'black'Text color when not focused.
focusedTextColorColorValue'black'Text color when focused or hovered.
backgroundColorColorValue'white'Button background color for the default state.
selectedBackgroundColorColorValue'white'Background color when the button is focused or hovered.
pressedBackgroundColorColorValue'white'Background color when the button is pressed.
rippleColorColorValue-Ripple color for the button press effect.
centerRipplebooleanfalseIf true, the ripple starts at the center of the button.
rippleDurationnumber-Duration of the ripple animation in milliseconds.
...PressablePropsOmit<PressableProps, 'onPress' | 'children' | 'style' | 'className'>-All other React Native Pressable props.

FlatInputProps

PropertyTypeDefaultDescription
All LabeledInputProps except labelStyle--Inherits all labeled input props except labelStyle.
labelStyle{ labelFilledFontSize?, labelFilledColor?, ...TextStyle }-Label style and filled state props.
inputStyleViewStyle (partial)-Style for the input view component.

LabeledInputProps

PropertyTypeDefaultDescription
onChange(text: string) => void-Called when the input text changes.
styleLabelInputStyle | (state: LabelInputState) => LabelInputStyle-Container style for layout properties.
labelStyle{ labelFilledOffset?, labelFilledFontSize?, labelFilledColor?, ...TextStyle }-Label style and filled state props.
textStyleTextStyle-Typography for label and placeholder text.
classNamestring-Container CSS class on web.
inputConfigInputConfigrequiredNative TextInput props plus web classes.
leftComponentReactElement | (state: LabelInputState) => ReactElement-Optional leading icon.
rightComponentReactElement | (state: LabelInputState) => ReactElement-Optional trailing icon.
backgroundColorColorValue-Background color.
selectedBackgroundColorColorValue-Background when selected.
pressedBackgroundColorColorValue-Background when pressed.

InputConfig (used by LabeledInputProps.inputConfig)

PropertyTypeDescription
classNamestringCSS class for the input on web.
placeholderClassNamestringCSS class for the placeholder on web.
...TextInputPropsAll standard React Native TextInput props except style, onFocus, onBlur, onPointerEnter, onPointerLeave, onChangeTextPass-through native input props.

DropdownProps

PropertyTypeDefaultDescription
dataT[]requiredItems to render in the dropdown.
onSelect(item: T, index: number) => void-Called on item selection.
onDropdownWillShow(willShow: boolean) => void-Called before opening or closing.
defaultValueT-Pre-selected value.
defaultValueByIndexnumber-Pre-selected index, zero-based.
disabledbooleanfalseDisable the entire dropdown.
disabledIndexesnumber[]-Disable specific rows.
disableAutoScrollbooleanfalsePrevent auto scroll to selection.
testIDstring-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.
dropDownSpacingnumber-Space between the trigger button and the dropdown window.
dropdownStyleViewStyle-Container style.
statusBarTranslucentboolean-Show under the Android status bar.
dropdownOverlayColorstring-Backdrop color.
showsVerticalScrollIndicatorboolean-Show the vertical scroll bar.
animateDropdownboolean-Enable opening and closing animation.
animationConfigAnimationConfig-Timing config when using timing animation.
springConfigWithSpringConfig-Spring config when using spring animation.
animationType'spring' | 'timing''spring'Choose the animation driver.
searchboolean-Enable the built-in search input.
searchInputStyleViewStyle-Search container style.
searchInputTxtColorstring-Search input text color.
searchInputTxtStyleViewStyle-Search input text style.
searchPlaceHolderstring-Search placeholder text.
searchPlaceHolderColorstring-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.

DropdownRef

MethodSignatureDescription
reset() => voidClear selection and search.
openDropdown() => voidOpen programmatically.
closeDropdown() => voidClose programmatically.
selectIndex(index: number) => voidSelect item by index.

Navigation types

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

Components details

Ripple

Visual press feedback effect available in BaseButton and other interactables. Enable via enableRipple and configure color/duration.

SpatialNavigationView

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

SpatialNavigationScrollView

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

SpatialNavigationFocusableView

Focusable wrapper that renders a View and exposes node state to children. See FocusableViewProps for the full API.

SpatialNavigationRoot

Top-level provider that enables spatial navigation, remote handling, and focus management.

Full props and usage notes: SPATIAL_NAVIGATION_API.md#spatialnavigationroot

SpatialNavigationNode

Low-level focusable node used internally by SpatialNavigationFocusableView. Exposes focus lifecycle events and can be referenced via SpatialNavigationNodeRef.

SpatialNavigationVirtualizedList

Virtualized list integrated with spatial navigation. Provides focus(index) and scrollTo(index) via ref.

SpatialNavigationVirtualizedGrid

Virtualized grid version exposing the same ref API as the list.

DefaultFocus

Marks a node as initially focused within a subtree when the root activates.

SpatialNavigationDeviceTypeProvider

Provider that detects device type (pointer/remote) and adapts focus interactions accordingly.


📜 Contributing and license

PRs and issues are welcome. See LICENSE for details (MIT).

Author: ImRoodyDev (https://github.com/imroodydev)

About

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.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages