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
19 days ago

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

Rumaiz Ahmed
Get notified by email when there are changes.