UI Components for React Navigation providing headers, buttons, and layout components with cross-platform support
—
Helper functions for calculating dimensions, resolving titles and labels, and other common navigation tasks with platform-specific optimizations.
Calculates platform-specific default header height considering device type, orientation, modal presentation, and platform-specific features like Dynamic Island.
/**
* Calculate platform-specific default header height
* @param layout - Screen layout dimensions
* @param modalPresentation - Whether header is in modal presentation
* @param topInset - Top safe area inset (for status bar, notch, etc.)
* @returns Calculated header height in pixels
*/
function getDefaultHeaderHeight(
layout: Layout,
modalPresentation: boolean,
topInset: number
): number;Usage Examples:
import { getDefaultHeaderHeight } from "@react-navigation/elements";
// Basic header height calculation
function MyScreen() {
const [layout, setLayout] = useState({ width: 375, height: 812 });
const { top: topInset } = useSafeAreaInsets();
const headerHeight = getDefaultHeaderHeight(
layout,
false, // Not modal
topInset
);
return (
<View style={{ paddingTop: headerHeight }}>
<Text>Content below header</Text>
</View>
);
}
// Modal header height
function ModalScreen() {
const layout = useFrameSize(frame => frame);
const { top: topInset } = useSafeAreaInsets();
const modalHeaderHeight = getDefaultHeaderHeight(
layout,
true, // Modal presentation
topInset
);
return (
<View>
<View style={{ height: modalHeaderHeight }}>
<Text>Modal Header</Text>
</View>
<View style={{ flex: 1 }}>
<Text>Modal Content</Text>
</View>
</View>
);
}
// Dynamic header height based on orientation
function OrientationAwareHeader() {
const [layout, setLayout] = useState({ width: 375, height: 812 });
const { top: topInset } = useSafeAreaInsets();
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => {
setLayout({ width: window.width, height: window.height });
});
return subscription?.remove;
}, []);
const headerHeight = getDefaultHeaderHeight(layout, false, topInset);
const isLandscape = layout.width > layout.height;
return (
<View style={{ height: headerHeight, backgroundColor: '#f8f9fa' }}>
<Text>
{isLandscape ? 'Landscape' : 'Portrait'} Header ({headerHeight}px)
</Text>
</View>
);
}Calculates optimal sidebar width based on screen dimensions following Material Design guidelines and responsive design principles.
/**
* Calculate optimal sidebar width based on screen width
* @param dimensions - Object containing screen width
* @returns Calculated sidebar width in pixels
*/
function getDefaultSidebarWidth({ width }: { width: number }): number;
/** Default drawer width constant */
const DEFAULT_DRAWER_WIDTH = 360;
/** Approximate app bar height constant */
const APPROX_APP_BAR_HEIGHT = 56;Usage Examples:
import { getDefaultSidebarWidth } from "@react-navigation/elements";
// Basic sidebar width calculation
function DrawerNavigator() {
const screenWidth = useFrameSize(frame => frame.width);
const sidebarWidth = getDefaultSidebarWidth({ width: screenWidth });
return (
<View style={{ flexDirection: 'row' }}>
<View style={{ width: sidebarWidth, backgroundColor: '#f5f5f5' }}>
<Text>Sidebar Content</Text>
</View>
<View style={{ flex: 1 }}>
<Text>Main Content</Text>
</View>
</View>
);
}
// Responsive sidebar
function ResponsiveSidebar() {
const screenWidth = useFrameSize(frame => frame.width);
const sidebarWidth = getDefaultSidebarWidth({ width: screenWidth });
const showSidebar = screenWidth > 768;
return (
<View style={{ flexDirection: 'row', flex: 1 }}>
{showSidebar && (
<View style={{
width: sidebarWidth,
backgroundColor: '#ffffff',
borderRightWidth: 1,
borderRightColor: '#e0e0e0'
}}>
<NavigationSidebar />
</View>
)}
<View style={{ flex: 1 }}>
<MainContent />
</View>
</View>
);
}
// Animated sidebar
function AnimatedSidebar({ isOpen }) {
const screenWidth = useFrameSize(frame => frame.width);
const sidebarWidth = getDefaultSidebarWidth({ width: screenWidth });
const animatedWidth = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(animatedWidth, {
toValue: isOpen ? sidebarWidth : 0,
duration: 300,
useNativeDriver: false
}).start();
}, [isOpen, sidebarWidth]);
return (
<Animated.View style={{
width: animatedWidth,
height: '100%',
backgroundColor: '#f8f9fa'
}}>
<SidebarContent />
</Animated.View>
);
}Resolves header title with priority logic, supporting both string titles and function-based title renderers.
/**
* Resolve header title with priority logic
* @param options - Object containing title options
* @param fallback - Fallback title if no options provide a title
* @returns Resolved title string
*/
function getHeaderTitle(
options: {
/** Simple title string */
title?: string;
/** Custom title renderer or string */
headerTitle?: string | ((props: HeaderTitleProps) => React.ReactNode);
},
fallback: string
): string;Usage Examples:
import { getHeaderTitle } from "@react-navigation/elements";
// Basic title resolution
function HeaderComponent({ route, options }) {
const title = getHeaderTitle(
{
title: route.params?.title,
headerTitle: options.headerTitle
},
route.name // fallback to route name
);
return (
<View>
<Text style={styles.headerTitle}>{title}</Text>
</View>
);
}
// Priority resolution example
function TitleResolver() {
// Priority: headerTitle > title > fallback
const title1 = getHeaderTitle(
{ headerTitle: "Custom Header", title: "Basic Title" },
"Fallback"
);
// Result: "Custom Header"
const title2 = getHeaderTitle(
{ title: "Basic Title" },
"Fallback"
);
// Result: "Basic Title"
const title3 = getHeaderTitle(
{},
"Fallback"
);
// Result: "Fallback"
return (
<View>
<Text>Title 1: {title1}</Text>
<Text>Title 2: {title2}</Text>
<Text>Title 3: {title3}</Text>
</View>
);
}
// Dynamic title resolution
function DynamicHeader({ route, navigation }) {
const [customTitle, setCustomTitle] = useState("");
const resolvedTitle = getHeaderTitle(
{
title: customTitle || route.params?.title,
headerTitle: route.params?.headerTitle
},
route.name
);
useLayoutEffect(() => {
navigation.setOptions({
title: resolvedTitle
});
}, [navigation, resolvedTitle]);
return (
<View>
<TextInput
value={customTitle}
onChangeText={setCustomTitle}
placeholder="Enter custom title"
/>
<Text>Current title: {resolvedTitle}</Text>
</View>
);
}Resolves label text with priority-based fallback support, commonly used for button and navigation labels.
/**
* Resolve label text with priority-based fallback
* @param options - Object containing label options
* @param fallback - Fallback label if no options provide a label
* @returns Resolved label string
*/
function getLabel(
options: {
/** Explicit label text (highest priority) */
label?: string;
/** Title to use as label (medium priority) */
title?: string;
},
fallback: string
): string;Usage Examples:
import { getLabel } from "@react-navigation/elements";
// Basic label resolution
function NavigationButton({ route, options }) {
const label = getLabel(
{
label: options.tabBarLabel,
title: options.title || route.params?.title
},
route.name // fallback to route name
);
return (
<TouchableOpacity>
<Text>{label}</Text>
</TouchableOpacity>
);
}
// Priority resolution examples
function LabelExamples() {
// Priority: label > title > fallback
const label1 = getLabel(
{ label: "Custom Label", title: "Screen Title" },
"Fallback"
);
// Result: "Custom Label"
const label2 = getLabel(
{ title: "Screen Title" },
"Fallback"
);
// Result: "Screen Title"
const label3 = getLabel(
{},
"Fallback"
);
// Result: "Fallback"
return (
<View>
<Text>Label 1: {label1}</Text>
<Text>Label 2: {label2}</Text>
<Text>Label 3: {label3}</Text>
</View>
);
}
// Tab bar implementation
function CustomTabBar({ state, descriptors, navigation }) {
return (
<View style={styles.tabBar}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = getLabel(
{
label: options.tabBarLabel,
title: options.title
},
route.name
);
const isFocused = state.index === index;
return (
<TouchableOpacity
key={route.key}
onPress={() => navigation.navigate(route.name)}
style={[
styles.tabItem,
isFocused && styles.tabItemFocused
]}
>
<Text style={[
styles.tabLabel,
isFocused && styles.tabLabelFocused
]}>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
);
}
// Button with dynamic label
function ActionButton({ action, title, label }) {
const buttonLabel = getLabel(
{ label, title },
"Action" // Default fallback
);
return (
<Button onPress={action}>
{buttonLabel}
</Button>
);
}The getDefaultSidebarWidth function follows these principles:
function AdaptiveLayout() {
const layout = useFrameSize(frame => frame);
const { top: topInset } = useSafeAreaInsets();
const headerHeight = getDefaultHeaderHeight(layout, false, topInset);
const sidebarWidth = getDefaultSidebarWidth(layout);
const showSidebar = layout.width > 768;
return (
<View style={{ flex: 1 }}>
<View style={{ height: headerHeight }}>
<HeaderComponent />
</View>
<View style={{ flex: 1, flexDirection: 'row' }}>
{showSidebar && (
<View style={{ width: sidebarWidth }}>
<SidebarComponent />
</View>
)}
<View style={{ flex: 1 }}>
<MainContent />
</View>
</View>
</View>
);
}All utility functions include proper error handling and default values:
// Safe usage with fallbacks
const headerHeight = getDefaultHeaderHeight(
layout || { width: 375, height: 812 },
modalPresentation ?? false,
topInset ?? 0
);
const sidebarWidth = getDefaultSidebarWidth({
width: screenWidth || 375
});
const title = getHeaderTitle(
options || {},
"Default Title"
);
const label = getLabel(
options || {},
"Default Label"
);Install with Tessl CLI
npx tessl i tessl/npm-react-navigation--elements