Floating Action Button and App Bar code

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.

Upvoters
Status

In Review

Board
💡

Feature Request

Date

19 days ago

Author

Rumaiz Ahmed

Subscribe to post

Get notified by email when there are changes.