react-native: zIndex does not work with dynamic components on Android

I am trying to render elements conditionally where each element has different zIndex style property. Using folowing code in Android emulator with react-native 0.30.0.

import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class Demo extends Component {
  constructor(props, ctx) {
    super(props, ctx);
    this.state = {
      showGreen: true,
    };
  }
  render() {
    return (
      <View style={{flex: 1, padding: 20}}>
        <View style={[styles.item, {zIndex: 3, backgroundColor: 'red'}]}>
          <Text>zIndex: 3</Text>
        </View>
        {this.state.showGreen &&
        <View style={[styles.item, {zIndex: 2, backgroundColor: 'green'}]}>
          <Text>zIndex: 2</Text>
        </View>
        }
        <View style={[styles.item, {zIndex: 1, backgroundColor: 'blue'}]}>
          <Text>zIndex: 1</Text>
        </View>
        <View style={styles.button}>
          <Text onPress={() => this.setState({ showGreen: !this.state.showGreen })}>
            Toggle green
          </Text>
        </View>
      </View>
    );
  }

}

const styles = StyleSheet.create({
  item: {
    marginTop: -20,
    height: 50,
    paddingTop: 22,
  },
  button: {
    backgroundColor: 'gray',
    marginTop: 30,
  }
});

Initial screen looks like expected:

zindex1

When I click ‘Toggle green’ button to dynamically show/hide green element it hides blue element instead and furthermore element’s text is missing:

zindex2

When I click the button again, red and green elements remains visible and the toggle button jumps down.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 42
  • Comments: 84 (5 by maintainers)

Commits related to this issue

Most upvoted comments

I’m experiencing this also, zIndex + position:absolute breaks in android

any updates on this? I am also experiencing this bug, with zIndex + position : absolute on android

you can finish the work without zIndex

Closing this since it is now fixed (sorry for not posting here I didn’t know about this issue).

This will make it in RN 0.45 (May) which should be in RC in a few days.

Can we just agree that this issue needs to be reopen NOW?

actually, elevation property worked on android, just remove zIndex property on android platform and make the component with elevation property is the last component.

ios: {
            zIndex: 1
        },
        android: {
            elevation: 1
        },

it works for me! thank all!

Instead of return null I just return <View /> and it works fine for me on both Android and iOS.

Adding “elevation: 100” to the style solved my issue. credit goes to Shukarullah Shah

0.54.0 The problem still exists

Same thing happened to me, so I’ll throw in some more details: In iOS, returning null makes the element disappear. In Android, you have to reduce the height to 0 and remove borders. What is worse is that you can’t stick to a single solution, because Android’s workaround won’t work for iOS. The element will just show up again.

Here’s the code for a component that does this sort of thing:

import React from 'react';

import {
    Image,
    Platform,
    StyleSheet,
    Text,
    TouchableOpacity,
    View
} from 'react-native';

import { default as closeIcon } from "../img/closeIcon.png";

const ReadFullArticleFloatingBox = ({visible, opacity = 0, title, onPressRead, onPressClose}) => {
    let conditionalLayout = visible ? {} : styles.hidden;
    return Platform.OS === "ios" && !visible ? null : (
        <View style={[styles.container, conditionalLayout, { opacity }]}>
            <View style={styles.leftContainer}>
                <Text numberOfLines={1} style={styles.title}>{title}</Text>
                <TouchableOpacity onPress={onPressRead} style={styles.readButton}>
                    <Text style={styles.readButtonText}>READ FULL ARTICLE</Text>
                </TouchableOpacity>
            </View>
            <TouchableOpacity onPress={onPressClose} style={styles.icon}>
                <Image
                        resizeMode={Image.resizeMode.contain}
                        style={styles.iconImage}
                        source={closeIcon}
                    />
            </TouchableOpacity>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        height: 60,
        position: "absolute",
        zIndex: 2,
        left: 0,
        right: 0,
        marginTop: 20,
        paddingHorizontal: 10,
        borderTopWidth: 1,
        borderBottomWidth: 1,
        borderColor: "lightgrey",
        backgroundColor: "white",
        flexDirection: "row",
        alignItems: "center",
        justifyContent: "center"
    },
    hidden: {
        zIndex: -2,
        height: 0,
        borderTopWidth: 0,
        borderBottomWidth: 0
    },
    leftContainer: {
        flex: 1,
        padding: 6
    },
    title: {
        fontSize: 13,
        fontFamily: "CrimsonText-Roman",
        color: "#46474C"
    },
    readButton: {
        paddingVertical: 6
    },
    readButtonText: {
        fontFamily: "Roboto",
        color: "teal",
        fontSize: 11
    },
    icon: {
        width: 40,
        height: 40,
        alignItems: "center",
        justifyContent: "center",
        padding: 10
    },
    iconImage: {
        width: 20,
        height: 20
    }
});

export default ReadFullArticleFloatingBox;

Any updates on this issue?

v0.54.2

Still a problem.

same here, zIndex + position : absolute, any update ?

My react-native version is 0.49.3 and I am still encountering this problem with the zIndex and the elevation in android. I also used the @glenn-axsy workaround but still couldn’t solve the issue. skype_picture 1

In the image below I want the user icon on top of the View but it is hiding and not coming on the top as expected

0.52.1 having the same issue

Why this issue is closed? zIndex still not working on Android, and elevation is not the solution, because of mess of pointer-events.

For anyone still stuck on the old RN here’s a function I use that can be spread into your styles. It’s based on the ideas presented by others above:

function zIndexWorkaround(val: number): Object {
    return Platform.select({
        ios: {zIndex: val},
        android: {elevation: val}
    });
}

Use it like this in place of e.g: zIndex: 100

...zIndexWorkaround(100)

Remove the Flow type checking from the function if you don’t need it

@janicduplessis I’ working on an app that is currently using RN 0.45.1 and it has a View with 3 images inside. I need left and right images to be on top of the center image, so I set a zIndex to the left image so its placed on top of the center one. It does not work. It only works when instead of using zIndex, I use elevation in Android. This way it does work, but I get the warning error saying Warning: Failed prop type: Invalid props.style key `elevation` supplied to `RCTImageView`.

This is my code

<View
  style={{
    flex: 2,
    justifyContent: 'center',
    alignItems: 'center',
    flexDirection: 'row',
  }}
>
  <Image
     source={require('./image_left.png')}
     style={{ top: 60, left: 50, elevation: 1 }} // zIndex doesn't work
  />
  <Image source={require('./image_center.png')} />
  <Image
     source={require('./image_right.png')}
     style={{ top: 100, left: -65 }}
   />
</View>

BTW, this is what I’m trying to do

images

I’m still experiencing this just for the record!

Still exist in v0.55.4

Thanks for your case @jaredmharris. However using elevation is not the same as zIndex, as pointer-events pass through what is underneath. elevation also causes a shadow to be created.

@mvf4z7 Just keep track of this PR: https://github.com/facebook/react-native/pull/10954

My local Android unit and integration tests passed. Circle and Travis CI failed for some unrelated reasons, so as soon as someone from Reviewers could have a look into this, maybe we will have it merged. I wouldn’t mind to have it asap as well 😉

Issue exists with react-native version 0.50.3 Works fine on iOS, compatibility issues with Android.

Still an issue in 0.32. It seems that zindex breaks component unmounting.

I think the issue is that zIndex was not updated properly after the view was mounted. #15203 should fix it, let me know if you can test it and it fixes your problem.

Can confirm what others have mentioned. I added the following to my style.alert and it works on both platforms now.

...Platform.select({
      ios: {zIndex: 9999},
      android: {elevation: 100}
    })

I’m also having an issue with zIndex + position: 'absolute' on Android. I have a dropdown menu component that always displays the currently selected item, and when it is activated, it also displays a list of all of the available dropdown menu items below this. The view that this dropdown menu is used in has other components as siblings of the dropdown menu component, and they are all after the dropdown menu component, since I want the currently selected item of the dropdown menu component to be shown first.

I’m trying to use zIndex so that the list of the available dropdown menu items is shown above the other components that are rendered after the dropdown menu. I’m simply applying a zIndex: 2 to the <View> that contains this list of available dropdown menu items to achieve this. The list of available dropdown menu items is rendered within a <View> that has position: 'absolute'.

This technique is working perfectly on iOS and behaves exactly as I expect/want. However, the exact same code is not working on Android; The list of available dropdown menu items is always rendered behind the other components that are rendered after the dropdown menu, and therefore is not visible. I’ve tried all sorts of variations of zIndex values for different components in the hierarchy, as well as elevation, and nothing seems to make this work on Android, even though it works fine on iOS.

Something must be different about how zIndex works in iOS vs Android, as the same code behaves differently in these two environments.

It’s been unresolved for too long, any updates?

Issue exists with react-native version 0.51.0 too

Got the same problem here ! After reading the discussions and skimming through the code I realized that the root problem lies in the current implementation of zIndex which is basically invoking bringToFront() on the child views in order of their zIndex. ( It is done in ViewGroupManager.reorderChildrenByZIndex() method)

This method spoils the natural index of child views in their parent. Therefore indicesToRemove parameter in NativeViewHierarchyManager.manageChildren will no longer refer to the actual child.

@asgvard did a beautiful job in PRs (#10954) trying to use tag-reference instead of child-index. However @astreet suggested the problem is better be solved fundamentally by migrating to a zIndex implementation mechanism that doesn’t mess with child-view indices (probably using android’s ViewGroup#getChildDrawingOrder).

It seems like 23 days ago @janicduplessis fixed zIndex mechanism in commit 9a51fa8. When I used react-native source from master branch everything was working just fine.

Hope this commit goes on the next release !

Same issue here, everything works as expected without a zIndex (It just doesn’t look right) Once I add a zIndex of 1 it goes to shit.

any updates?

UPDATE:

First of all, I found out that it has nothing to do with the reorderChildrenByZIndex() at all, because we actually don’t need to reorder children after we remove something, because the relative zIndex order for remaining items remain the same. The problem was exactly in that it was removing the wrong View by it’s index in the array.

Fixed by relying on tagsToDelete instead of indicesToRemove in manageChildren method of NativeViewHierarchyManager. Because after reordering we cannot rely on the item index in the ViewGroup, but the tag seems to be reliable way of identifying the View anytime. I think the similar reason of using tagsToDelete was applied here.

Also this issue happens when Adding views, it adds them in the wrong index 😃 But since after adding the views we’re doing reorder again, it doesn’t matter that it was added into the wrong place.

So the proposed solution is to change manageChildren method, so it will first make a loop by tagsToDelete, perform removeView() on all the tagsToDelete, then after the cleanup of all the nodes to delete, we perform a loop by viewsToAdd and that’s it. It will remove the huge chunk of code from here to here, because essentially all it’s doing is viewManager.removeViewAt(viewToManage, indexToRemove); only in the case if the node is not animated and not in the tagsToDelete array. So instead we could just move this responsibility to the loop by tagsToDelete.

I will prepare PR soon 😃

If it’s helpful to anyone, I created a Hideable component to handle this:

import React, { Component } from 'react';
import {
  View,
  StyleSheet,
  Platform
} from 'react-native';

const hideable_styles = StyleSheet.create({
  android_hidden: {
    height: 0,
    borderWidth: 0,
    paddingTop: 0,
    paddingBottom: 0,
    marginTop: 0,
    marginBottom: 0
  }
});
class Hideable extends Component {
  render(){
    const {hide, children, style = {}, ...rest_props} = this.props;

    // on ios, best way to hide is to return null
    if (hide && Platform.OS === 'ios') return null;

    // otherwise, if android, going to add in the special android hidden styles
    const conditional_layout = hide ? hideable_styles.android_hidden : {};
    const styles = [style, conditional_layout];

    return (
      <View {...rest_props} style={styles}>
        {children}
      </View>
    );
  }
}

export default Hideable;

Here’s a simple use:

const simpleTestHideable = ({hide = true}) => {
  return (
    <Hideable hide={hide} style={{zIndex: 1, backgroundColor: 'red', height: 100}} >
      <Text>{hide ? 'You should not see this' : 'You should see this' }</Text>
    </Hideable>
  );
}

Here’s a more complex, interactive usage example:

class TestHideable extends Component {
  constructor(props){
    super(props);

    this.state = { hide_content: false };

    this.toggleContent = this.toggleContent.bind(this);
  }

  toggleContent(){
    this.setState({hide_content: !this.state.hide_content});
  }

  render(){
    const { hide_content } = this.state;

    return (
      <View style={{paddingTop: 20}}>
        <Hideable
          hide={hide_content}
          anotherProp="foo"
          style={{
            padding: 10,
            height: 100,
            borderWidth: 0.5,
            borderColor: '#d6d7da',
            marginTop: 100,
            marginBottom: 100,
            backgroundColor: 'red',
            zIndex: 2
          }} >
            <Text>I am showing!</Text>
        </Hideable>
        <Text onPress={this.toggleContent}>{hide_content ? 'SHOW!' : 'HIDE!'}</Text>
      </View>
    )
  }
}

export default TestHideable;

You should be able to pass through whatever styles or props you’d like to Hideable’s View, make it animatable, etc.

Also, @tioback, I noticed you set a negative zIndex, is that something you recommend? It’s worked for me without it but I wasn’t sure if it was needed for certain devices or certain use cases. I also noticed that on my device I had to remove the vertical margins and padding as well, so there may be other styles to watch out for.

I’m still pretty new to React Native so, grain of salt.

+1. Works great on ios, breaks views in android

My RN version is 0.49.5 and I’ve encountered the same issue when setting the position to absolute. My workaround is rearranging the components, whose orders are based on the zIndex. This may introduce some chaos but should be a better approach than setting the elevation.

@glenn-axsy and others reading his comment, just a warning: elevation is not a one-to-one solution for zIndex. For me it never worked how I needed it, I would get shadows when I didn’t, and also the pointerEvents order was incorrect.

Fortunately I did not have to deal with pointer-events with my code. I popup an alert on the screen that disappears after a few seconds.

Are you using a Modal component? You shouldn’t need to set zIndex/elevation for that. Make sure the order of your components are ordered correctly. Children components appear on top of parent components so see if you can change things up to get it to work.

A sincere thank you for trying your hand to fix it @janicduplessis - it is a blocker issue for the app I am working on.

Yo I’m the author of that commit 😃

@asgvard Thanks for the fantastic bug reporting. I think you’re right, check out:

https://github.com/facebook/react-native/blob/9ee815f6b52e0c2417c04e5a05e1e31df26daed2/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java#L123

I think the fix is as simple as matching the addView method so it looks like:

  public void removeViewAt(T parent, int index) {
    parent.removeViewAt(index);
    reorderChildrenByZIndex(parent)
  }

I don’t really have the bandwidth to PR a fix and test it right now, but you probably could 😃 It’s a good first PR.