React Native Vector Icons to fit the parent container without specifying a size prop (Expo)

Viewed 1213

I am using the Vector Icons provided by Expo https://github.com/expo/vector-icons

All the icons accept a size prop which is an integer, my use case is to see if there to adjust the icon size automatically based on the parent container without having to worry about the size on each and every device (because size seems to translate the same on every device despite the pixel ratio, dimensions etc)

<View style={{flex: 1}}>
  <Icon name="app-logo" size={30} color="white" />
</View>    

I tried setting style prop to the below options but no luck. I would expect that this will not work because after all they are just fonts and they need not support the width and height

<Icon name="app-logo" style={{flex: 1}} color="white" />
<Icon name="app-logo" style={{width: '100%', height: '100%' color="white" />

Is there any cleaner or suggested way to do this? Using dimensions and ratios of the devices is my last resort.

1 Answers

Here's an example of the closest I've been able to get:

best-expo-vector-icon-size-to-parent-attempt

import React from "react"
import { PixelRatio, StyleSheet, Text, View } from "react-native"
import { FontAwesome5 } from "@expo/vector-icons"

const getRandomNumber = (min: number, max: number) =>
  Math.random() * (max - min) + min

const App = () => {
  const [containerSize, setContainerSize] = React.useState<number>()

  const randomDimension = React.useMemo(() => getRandomNumber(100, 400), [])

  return (
    <View
      style={{
        flex: 1,
        backgroundColor: "green",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <View
        style={{
          width: randomDimension,
          aspectRatio: 1,
          backgroundColor: "red",
        }}
        onLayout={layoutEvent =>
          setContainerSize(layoutEvent.nativeEvent.layout.width)
        }
      >
        <FontAwesome5
          name="app-store-ios"
          size={containerSize || 0}
          style={{
            textAlign: "center",
          }}
          color="blue"
        />
      </View>
    </View>
  )
}

export default App

There's some code in here for intentionally setting the icon's parent container to a randomly sized square, and setting that on the parent, which is not what you're looking for but helped me re-create the problem.

The interesting bit which I think you want is the value passed to the onLayout prop of the icon's parent container, which gets the parent container's size and puts it in state. That state will be empty until the first render & layout, so we have the size of the vector icon defaulted to 0 until that happens. Once we have the size of the parent in state, we can set the size of the vector icon.

The vector icon doesn't actually fill the entire parent's space, but it scales pretty close pretty well. I think that's because the vector icon itself is a font under the hood (?) and we're trying to set the size of a font (in pts really (I think?)) to the size we got from the parent in pixels. There's probably a neat way to clean that up using react native's PixelRatio - I spent a bit of time trying but couldn't get it working perfectly.

The last thing I needed to do (since the vector icon doesn't actually "fill" the parent view was set the style prop of the icon to { textAlign: 'center' } which keeps it centered horizontally within the parent view (it was already centered vertically) instead of being off on the left side of the parent.

You could jank the whole thing into submission by adding a constant to the size and putting a negative top margin on the icon, but that's kind of a PITA and obviously not a great solution (it'll be a headache every time you do it and it probably won't work cross platform / on different pixel densities).


The reason I actually arrived at this post was because I was trying to keep an icon the same size as text; I was working on an app which showed a comment count (integer) to the left of a little chat bubble icon and I wanted the icon's size to scale with the system's font size if the user had adjusted that in their system settings. This is actually easier (which makes sense given my theory about vector icons being represented as fonts under the hood is correct). Here's the relevant part of my code that achieved that:

const StoryCommentsLabel = ({ story }: { story: HackerNewsItem }) => {
  const navigation = useNavigation()
  const numberOfComments = story.descendants || 0
  const doNotActTouchable = { activeOpacity: 1 }
  const fontScale = React.useMemo(() => PixelRatio.getFontScale(), [])
  const defaultFontSize = 14
  const iconSize = defaultFontSize * fontScale
  return (
    <TouchableOpacity
      style={{ flexDirection: "row", justifyContent: "flex-end" }}
      onPress={() => {
        if (numberOfComments > 0) {
          navigation.navigate("Story Comments", { story })
        }
      }}
      hitSlop={{ top: 20, left: 20, bottom: 20, right: 20 }}
      {...(numberOfComments === 0 ? doNotActTouchable : {})}
    >
      <Text
        style={{
          fontSize: 14,
          color: PlatformColor("secondaryLabel"),
        }}
      >
        {numberOfComments}
      </Text>
      <View style={{ width: 5 }} />
      <Ionicons
        name="chatbubble"
        size={iconSize}
        color={PlatformColor("secondaryLabel")}
      />
    </TouchableOpacity>
  )
}

There's more in there than you need, but the important bit is this:

  const fontScale = React.useMemo(() => PixelRatio.getFontScale(), [])
  const defaultFontSize = 14
  const iconSize = defaultFontSize * fontScale
Related