I have messing around with Hero UI Native for a bit and was able to come up with a couple of components that you guys don’t have yet, But I am not sure to how contributions work or anything.
But i have included the components code below hopefully this helps:
FAB:
import { Button, type ButtonRootProps } from 'heroui-native';
import React, { type ReactNode } from 'react';
import { ViewStyle } from 'react-native';
import Animated, {
FadeInDown,
FadeOutDown,
LinearTransition,
type BaseAnimationBuilder,
type EntryExitAnimationFunction,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface UniversalFABProps {
/** Function called when the FAB is pressed */
onPress: () => void;
/** Icon component to render */
icon: ReactNode;
/** Optional text label for Extended FAB pattern */
label?: string;
/** HeroUI variant */
variant?: ButtonRootProps['variant'];
/** HeroUI size */
size?: ButtonRootProps['size'];
/** Additional styling for the button itself */
className?: string;
/** Base Tailwind classes for positioning (excluding bottom offset) */
containerClassName?: string;
/** * Extra vertical offset.
* Useful if you have a TabBar height to account for.
* Default is 16.
*/
bottomOffset?: number;
entering?: BaseAnimationBuilder | EntryExitAnimationFunction;
exiting?: BaseAnimationBuilder | EntryExitAnimationFunction;
}
/**
* FloatingActionButton - Adapts dynamically to Safe Areas and Tab Bars.
*/
export const FloatingActionButton = ({
onPress,
icon,
label,
variant = 'secondary',
size = 'lg',
className = 'rounded-xl shadow-lg',
containerClassName = 'absolute right-5', // Removed bottom-8
bottomOffset = 16,
entering = FadeInDown.delay(200).springify(),
exiting = FadeOutDown.duration(200),
}: UniversalFABProps) => {
const insets = useSafeAreaInsets();
/**
* The 'bottom' value is the safe area inset (which includes TabBar height
* if wrapped in a safe provider) plus our custom padding.
*/
const animatedContainerStyle: ViewStyle = {
bottom: Math.max(insets.bottom, 16) + bottomOffset,
};
return (
<Animated.View
entering={entering}
exiting={exiting}
layout={LinearTransition.springify()}
className={containerClassName}
style={animatedContainerStyle}
>
<Button
className={className}
variant={variant}
size={size}
onPress={onPress}
isIconOnly={!label}
pressableFeedbackVariant="ripple"
>
{icon}
{label && (
<Button.Label className="ml-2 font-bold">{label}</Button.Label>
)}
</Button>
</Animated.View>
);
};
App-Bar:
import { clsx, type ClassValue } from 'clsx';
import { BlurView } from 'expo-blur';
import * as Haptics from 'expo-haptics';
import * as React from 'react';
import { Platform, Pressable, Text, View, useColorScheme } from 'react-native';
import Animated, {
FadeInDown,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type AppBarAction = {
id: string | number; // Stable identifier for use as React key
icon: React.ReactNode; // Flexible: pass any icon component here
onPress: () => void;
badge?: number;
};
interface AppBarProps {
children?: React.ReactNode;
title?: string;
subtitle?: string;
actions?: AppBarAction[];
leading?: React.ReactNode;
trailing?: React.ReactNode;
elevation?: 0 | 1 | 2;
variant?: 'surface' | 'primary' | 'transparent';
onLeadingPress?: () => void;
className?: string;
}
const AppBar = React.forwardRef<View, AppBarProps>(
(
{
title,
subtitle,
actions = [],
leading,
trailing,
elevation = 0,
variant = 'surface',
onLeadingPress,
className,
...props
},
ref,
) => {
const insets = useSafeAreaInsets();
const isDark = useColorScheme() === 'dark';
const height = 64;
const totalHeight = height + insets.top;
return (
<Animated.View
ref={ref}
entering={FadeInDown.duration(400)}
style={{ paddingTop: insets.top, height: totalHeight }}
className={cn(
'w-full z-50',
variant === 'surface' && 'bg-background',
variant === 'primary' && 'bg-primary',
elevation === 1 && 'border-b border-border',
elevation === 2 && 'shadow-lg bg-background',
className,
)}
{...props}
>
{variant === 'transparent' && (
<BlurView
intensity={Platform.OS === 'ios' ? 80 : 100}
tint={isDark ? 'dark' : 'light'}
className="absolute inset-0"
/>
)}
<View className="flex-1 flex-row items-center px-4">
{/* Leading Section */}
<View className="min-w-10 items-start">
{leading ||
(onLeadingPress ? (
<ActionButton onPress={onLeadingPress}>
{/* Fallback back icon or pass one in leading */}
{props.children}
</ActionButton>
) : null)}
</View>
{/* Center Title Section */}
<View className="flex-1 justify-center items-center">
{title && (
<Text
numberOfLines={1}
className={cn(
'text-lg font-semibold',
variant === 'primary'
? 'text-primary-foreground'
: 'text-foreground',
)}
>
{title}
</Text>
)}
{subtitle && (
<Text numberOfLines={1} className="text-xs text-muted-foreground">
{subtitle}
</Text>
)}
</View>
{/* Trailing Actions */}
<View className="min-w-10 flex-row items-center justify-end gap-x-2">
{trailing ||
actions.map((action) => (
<ActionButton
key={action.id}
onPress={action.onPress}
badge={action.badge}
>
{action.icon}
</ActionButton>
))}
</View>
</View>
</Animated.View>
);
},
);
// For the AppBar component
AppBar.displayName = 'AppBar';
const ActionButton = ({
onPress,
children,
badge,
}: {
onPress?: () => void;
children: React.ReactNode;
badge?: number;
}) => {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const handlePress = () => {
if (Platform.OS !== 'web')
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
onPress?.();
};
return (
<Pressable
onPressIn={() => (scale.value = withSpring(0.9))}
onPressOut={() => (scale.value = withSpring(1))}
onPress={handlePress}
hitSlop={12}
className="h-10 w-10 items-center justify-center rounded-full active:bg-muted/20"
>
<Animated.View style={animatedStyle}>{children}</Animated.View>
{!!badge && (
<View className="absolute -top-1 -right-1 bg-destructive rounded-full min-w-4.5 h-4.5 items-center justify-center px-1">
<Text className="text-[10px] font-bold text-white">
{badge > 99 ? '99+' : badge}
</Text>
</View>
)}
</Pressable>
);
};
// If you exported ActionButton as a separate component
ActionButton.displayName = 'ActionButton';
export default AppBar;
Please authenticate to join the conversation.
In Review
Feature Request
4 months ago

Rumaiz Ahmed
Get notified by email when there are changes.
In Review
Feature Request
4 months ago

Rumaiz Ahmed
Get notified by email when there are changes.