Skip to content

ImRoodyDev/react-native-cross-elements

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 version npm downloads TypeScript Reanimated


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

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>
	);
}

🧭 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

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>
)
	;
}

Dropdown

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>
	);
}

Switch

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} />;
}

FlatLabelInput, LabeledInputField, LabeledInputFieldWeb

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

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>}
		/>
	);
}

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)

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>
	);
}

Example: global toast

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>
	);
}

Example: anchored overlay/popover

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>
	);
}

Multiple hosts

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>

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

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.

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.
type PressableState = PressableStateCallbackType & {
	readonly focused: boolean;
	readonly hovered: boolean;
};

BaseButtonProps

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.

FlatInputProps

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.

LabeledInputProps

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.

InputConfig (used by LabeledInputProps.inputConfig)

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.

DropdownProps

Property Type Default Description
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

Method Signature Description
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

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors