Android Ripple Effect - Analyzed
React Native has a component Pressable
. Used like so <Pressable>clickable thing</Pressable>
. It is a more abstract way to build a button or something that a user can click. The component Pressable
has one Android-specific attribute android_ripple
, which allows to customize the UX of the ripple effect. I think the visual feedback the ripple effect provides is very user-friendly, it indicates if a click was detected. So I am investing time in making it work well, but it's not that easy. Let me share my learnings.
What is the Ripple Effect?
The ripple effect is the red dot flying in on the button. Can you see it? This is a native Android feature provided by React Native to be kinda controlled from there.
Contents
- The Context
- No
onPress
no Ripple - Background Color on a Child Overlays Ripple Effect
- Workaround: How to still use a Background Color?
- Don't add
borderless
! - Add
borderRadius
- Works - The Ripple Radius
- Building a Round Button
- Conclusion
The Context
It's all about React Native, correct. I am currently using v0.63.2 (just in case someone case along, reads this and screams "it's all wrong", check the version first). My Android version is API Level 28 and the device I use is a Cosmo Communicator made by Planet Computers, that's also why you saw a landscape video above.
No onPress
no Ripple
The <Pressable>
must receive a prop onPress
, if that one is missing I got no the ripple effect.
Expectation: I had expected onPress
can be left out, ripple still works.
// No `onPress`, NO ripple effect visible!
<Pressable
android_ripple={{color: 'red'}}
>
<Text>Click me</Text>
</Pressable>
// The `onPress` exists, ripple effect visible.
<Pressable
onPress={() => {}}
android_ripple={{color: 'red'}}
>
<Text>Click me</Text>
</Pressable>
Even a onPress={noop}
works. So I assume the argument is just checked, I didn't dive into the source code (but it's probably somewhere in there).
Learning: Always pass at least a noop
to onPress to make sure the ripple effect is visible.
Background Color on a Child Overlays Ripple Effect
The ripple effect is turned off by any child inside a <Pressable>
that has a background color.
Expectation: I did not expect this. I had expected the ripple effect to overlay any child component, no matter how it is styled.
// The ripple effect is NOT visible.
<Pressable
onPress={() => {}}
android_ripple={{color: 'red'}}
>
<Text
style={{backgroundColor: 'blue'}}
>Click me</Text>
</Pressable>
The above code just disables the ripple effect basically. So you click and you get no visual feedback if you clicked or not.
The below code shows, that the ripple effect still takes place, just behind the text component. There is an opacity on the text, which lets the ripple effect shine through and the colors mix.
// The colors red and blue mix, 50/50, so you can see the ripple effect is behind the text component.
<Pressable
onPress={() => {}}
android_ripple={{color: 'red'}}
>
<Text
style={{backgroundColor: 'blue', opacity: 0.5}}
>Click me</Text>
</Pressable>
If you give the Pressable a height, higher than the child element, here the text, you will also see the ripple effect show in the parts that are not covered by the child element.
<Pressable
onPress={() => {}}
android_ripple={{color: 'red'}}
style={{height: 100}}
>
...
Workaround: How to still use a Background Color?
I want the content to have a background color though. So I moved the background color to the Pressable component. That works.
<Pressable
onPress={() => {}}
android_ripple={{color: 'red'}}
style={{
backgroundColor: 'blue',
}}
>
<Text>Click me</Text>
</Pressable>
But it also creates one interesting effect, the color of the ripple effect is not pure red, as defined below, but it is red and blue mixed together.
Expectation: I would not have expected the colors to mix, I thought the ripple effect was a solid color.
Don't add borderless
!
When you add the attribute borderless
to the android_ripple
prop the background color is gone.
Expectation: I did not expect the borderless
attribute to influence how the component is styled.
// The attriute `borderless=true` REMOVES the background color!
<Pressable
onPress={() => {}}
android_ripple={{color: 'red', borderless: true}}
style={{
backgroundColor: 'blue',
}}
>
...
Add borderRadius
- Works
I also have the requirement to make the pressable a circle, so I will use borderRadius
. I did not expect this to work, but it does. No problem here.
<Pressable
onPress={() => {}}
android_ripple={{color: 'red'}}
style={{
backgroundColor: 'blue',
borderRadius: 10,
}}
>
The Ripple Radius
In the docs I did not find any info on what the radius
attribute on the android_ripple
prop really does. It says "Defines the radius of the ripple effect." ... doh, sorry but I thought that it was some kinda radius. But I figured it out.
// A big circle animates into a tiny one, of 1 pixel.
<Pressable
android_ripple={{color: 'red', radius: 1}}
// A big circle animates into a 20 pixels big one, that's where the animation ends.
<Pressable
android_ripple={{color: 'red', radius: 20}}
It seems that the start of the animation is always about 50dp (just an estimate). Once you click, it animates to the size you have set to radius
.
If you set radius=1
you see an animation from 50dp down to 1dp, a circle that becomes smaller over time.
If you have radius=50
it kinda looks like there is a static circle on the screen for a short amount of time.
If you have radius=100
it looks the entire circle stays at 100dp and just fades in and disappears. Nothing fancy. But does the job.
That seems to me like, the property should be called endRadius
or animationEndRadius
or something.
Building a Round Button
My actual goal was to build a round burger menu item with a proper ripple effect. Now that I explored the ripple effect (and React Native's use) in depth I am ready to use it. It looks like this right now:
const diameter = 100;
const circle = {
backgroundColor: 'white',
padding: diameter / 4,
borderRadius: diameter / 2,
};
return (
<Pressable
onPress={onPress}
android_ripple={{color: 'red', radius: 20}}
style={circle}
>
<IconBurgerMenu width={diameter / 2} height={diameter / 2} />
</Pressable>
);
Conclusion
I am not sure if I am not understanding the docs, expecting wrong things or simply not getting React Native yet. But I have a feeling that there are a couple of gotchas, that might also be called bugs. I will link it in the according places, maybe it feels helpful to someone.
UPDATE: It looks like there is an even better solution coming. It is in a pull request which basically puts the ripple effect always in the foreground, that's how I understand it. I think this might solve most of the issues I described above. Cool.