---
title: How to bring your React Native apps to life using sensors
authors: Enzo Manuel Mangano
published: October 22, 2024
categories: Development
tags: Reanimated, Skia
---

_This is a guest post from Enzo Manuel Mangano - he's a Software Engineer and [Educator](https://reactiive.io/) who is deeply passionate about pushing the boundaries of React Native animations._

...

More than 2.2 million apps are available on the Google Play Store and over 2 million apps on the App Store. Every day, more than [1000 new apps are released on both platforms](https://42matters.com/stats).



_So how are you supposed to make your app stand out?_



Many factors contribute to a successful app but one of them is the obsessive pursuit of a great user experience. Your app has to be fun and engaging.



You must consistently delight your users and search for that "wow" moment in every single interaction.



With this article, we'll explore how you can easily leverage your device's sensors to add that "extra something" to your app. 

## **Here's the plan**

To keep things practical, here's our goal: building a square that follows the device's movement. 

[Video: Linear's app welcome screen](https://youtu.be/QUbi52TaqUo)

The animation is clearly inspired by [Linear's app welcome screen](https://linear.app), which has quickly become a benchmark in app design.

[Tweet](https://x.com/60fpsdesign/status/1836812918635286959)

### Source code

Before diving into the article, you might want to check out the source code on GitHub, which contains the complete implementation of the tutorial and might be helpful to follow along. 

Here's the link: [https://github.com/enzomanuelmangano/expo-sensors-demo](https://github.com/enzomanuelmangano/expo-sensors-demo)

### The recipe

Each animation has a unique recipe. You need to gather the right ingredients and follow a precise recipe to make it work.

- The [useAnimatedSensor](https://docs.swmansion.com/react-native-reanimated/docs/device/useAnimatedSensor) hook from React Native Reanimated: it lets you create animations based on data from the device's sensors
- [React Native Skia](https://shopify.github.io/react-native-skia/): a package that brings the Skia Graphics Library to React Native. It'll be needed to animate shadows and to build the gradients.

### Setting up the first brick

To get started, we'll create a basic expo project and install the dependencies:

```bash
npx expo install @shopify/react-native-skia react-native-reanimated
```

Then we'll start by adding a black square in the center of the screen.

```tsx
const App = () => {
  return (
    <View style={styles.container}>
      <Animated.View style={styles.square} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  square: {
    height: SquareSize,
    width: SquareSize,
    backgroundColor: 'black',
    borderRadius: 35,
    borderCurve: 'continuous',
  },
});
```

It may not seem like much, but don't forget that before Pinocchio came to life, he was merely a block of wood.

![The base square is now in place](https://cdn.sanity.io/images/9r24npb8/production/7f37ee654d2b28f764ff7e75fc05e60c356386a6-2160x2160.jpg)

## Finding the key points

Before proceeding, we must first observe the device's movement to identify the key points of the animation. Essentially, we need to experience the animation from the user's perspective.

### Rotation around the Y-axis

We can easily identify three key points that will be the base of our animation on the Y-axis:

- When the device is in its "resting" position
- When the device is slightly rotated on the left side
- When the device is slightly rotated on the right side

![Y-axis observations](https://cdn.sanity.io/images/9r24npb8/production/81e2d8e2350c8b6186f44dab55357c7ef4a92745-3840x2160.png)

_But how this will be translated into values?_

Luckily, Reanimated's documentation provides an amazing Playground that we can use to find out the exact values.

![Reanimated playground](https://cdn.sanity.io/images/9r24npb8/production/c21a79422539c54a99f14bb2b06b3dc2eca26e87-1352x787.png)

With this tool, we can directly visualize the values we'll get from the useAnimatedSensor hook by using the "Rotation" sensor.

In detail, we're going to have direct access to Euler angles which are:

- **roll**: rotation around the front/back axis
- **pitch**: rotation around the left/right axis
- **yaw**: rotation around the vertical axis

Roughly speaking, we're going to use the "roll" to understand if the device is in its resting position (roll = 0), if it's slightly rotated on the left side (roll = -1), or if it's slightly rotated on the right side (roll = 1).

With this in mind, we can now transpose this knowledge into code and start building our animation.

```tsx
  const deviceRotation = useAnimatedSensor(SensorType.ROTATION, {
    interval: 20,
  });

  const rotateY = useDerivedValue(() => {
    const { roll } = deviceRotation.sensor.value;

    return interpolate(
      roll,
      [-1, 0, 1],
      [Math.PI / 8, 0, -Math.PI / 8],
      Extrapolation.CLAMP,
    );
  });

  const rStyle = useAnimatedStyle(() => {
    return {
      transform: [{ perspective: 200 }, { rotateY: `${rotateY.value}rad` }],
    };
  });
```

The main idea behind this code is that we're using the _roll_ value to interpolate the _rotateY_ value.

Here's how it works:

- When the device is slightly rotated on the left side, the _roll_ value is -1 and the _rotateY_ value is mapped to_ Math.PI / 8_
- When the device is in its resting position, the _roll_ value is 0 and the _rotateY_ value is mapped to _0_
- When the device is slightly rotated on the right side, the _roll_ value is 1 and the_ rotateY_ value is mapped to _-Math.PI / 8_

This interpolation allows for a smooth transition between these states, creating a realistic rotation effect based on the device's orientation.

![Completed Y-axis rotation](https://cdn.sanity.io/images/9r24npb8/production/25b9f911eb2ccf2bb6062a904160f727030b30ed-3840x2160.png)

### Rotation around the X-axis

Overall, the process of finding out the key points and the values to use for the X-axis is the same as we've just seen for the Y-axis.

We need to identify some keyframes:

- When the device is in its "resting" position
- When the device is slightly rotated down
- When the device is slightly rotated up

The only difference is the sensor we're using. In this case, we'll be using the _gravity_ sensor.

![X axis observations](https://cdn.sanity.io/images/9r24npb8/production/5cd3918520f71aee779b031491de394d9e5fcb6c-3840x2160.png)

As we did for the Y axis, we can use the Reanimated Playground to find out the exact values to use for our animation.

![Using the playground to find exact values](https://cdn.sanity.io/images/9r24npb8/production/070e1a68d8a45a38edab53fe0623a9644e06fffe-1352x787.png)

We can refer to the "z" value to understand if the device is in its resting position (z=-6), if it's slightly rotated down (z=-9), or if it's slightly rotated up (z=-1).

And here comes the dancing square 🕺🏼

```tsx
  const rotationGravity = useAnimatedSensor(SensorType.GRAVITY, {
    interval: 20,
  });

  const rotateX = useDerivedValue(() => {
    const { z } = rotationGravity.sensor.value;

    return interpolate(
      z,
      [-9, -6, -1],
      [-Math.PI / 8, 0, Math.PI / 8],
      Extrapolation.CLAMP,
    );
  });

  const rStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { perspective: 200 },
        { rotateY: `${rotateY.value}rad` },
        { rotateX: `${rotateX.value}rad` },
      ],
    };
  });
```

[Video: iOS dancing square](https://youtu.be/HLPQ9Q-NHgc)

## Where are the shadows?

We finally have the feeling that our app is breathing, but there's something missing. Our square is not casting shadows and it doesn't feel like it's interacting with the real world.

This is where React Native Skia comes into play.

### 1. Moving everything to Skia

Unfortunately, we can't just start adding shadows to our square but to use Skia we need to refactor our code so that it's defined within a Skia Canvas.

```tsx
import { Canvas, Group, RoundedRect, vec } from '@shopify/react-native-skia';

const CanvasSize = {
 width: 500,
 height: 500,
};

const CanvasCenter = vec(CanvasSize.width / 2, CanvasSize.height / 2);

const App = () => {

 const rTransform = useDerivedValue(() => {
   return [
     { perspective: 200 },
     { rotateY: rotateY.value },
     { rotateX: rotateX.value },
   ];
 });

 return (
   <View style={styles.container}>
     <Canvas
       style={{
         height: CanvasSize.height,
         width: CanvasSize.width,
       }}>
       <Group origin={CanvasCenter} transform={rTransform}>
         <RoundedRect
           x={CanvasCenter.x - SquareSize / 2}
           y={CanvasCenter.y - SquareSize / 2}
           width={SquareSize}
           height={SquareSize}
           color="#101010"
           r={35}
         />
       </Group>
     </Canvas>
   </View>
 );
};
```

This won't change anything visually, but it's a necessary step to be able to add shadows later on.

### 2. Adding gradients (in-between step)

Since we're now using Skia we can enhance the whole experience by adding a Background RadialGradient.

```tsx
// ... existing code 
 return (
   <View style={styles.fill}>
     <Canvas style={StyleSheet.absoluteFill}>
       <Fill>
         <RadialGradient
           c={vec(windowWidth / 2, windowHeight / 2)}
           r={windowWidth / 1.5}
           colors={['#252525', '#000000']}
         />
         <Blur blur={50} />
       </Fill>
     </Canvas>
     <View style={styles.container}>
       {...}
     </View>
   </View>
 );
};
// ... existing code 
```

Here's the result - Can you hear the square screaming** "Where are my shadows?!"**.

![Where are my shadows!?](https://cdn.sanity.io/images/9r24npb8/production/4e03f61910d4b91eef71b377cb70f249c814e531-2880x2160.jpg)

### 3. Shadows

Right now, we can finally start defining the shadows.

In detail, we're going to animate a couple of values:

1. _**dx** (delta x)_: This represents the horizontal offset of the shadow. A positive value moves the shadow to the right, while a negative value moves it to the left.
2. _**dy** (delta y)_: This represents the vertical offset of the shadow. A positive value moves the shadow downwards, while a negative value moves it upwards.

```tsx
const App = () => {
// ... existing code 
 const shadowDx = useDerivedValue(() => {
   return interpolate(
     rotateY.value,
     [-Math.PI / 8, 0, Math.PI / 8],
     [10, 0, -10],
     Extrapolation.CLAMP,
   );
 });

 const shadowDy = useDerivedValue(() => {
   return interpolate(
     rotateX.value,
     [-Math.PI / 8, 0, Math.PI / 8],
     // Exception instead of (-10 use 7) that's because the "light source" is on the top
     [7, 0, 10],
     Extrapolation.CLAMP,
   );
 });
// ... existing code 
}
```

Then we can add the Shadow components to our square and let the magic happen.

```tsx
// ... existing code 
 return (
   <View style={styles.fill}>
     {...} {/* The background RadialGradient */}
     <View style={styles.container}>
       <Canvas
         style={{
           height: CanvasSize.height,
           width: CanvasSize.width,
         }}>
         <Group origin={CanvasCenter} transform={rTransform}>
           <RoundedRect
             x={CanvasCenter.x - SquareSize / 2}
             y={CanvasCenter.y - SquareSize / 2}
             width={SquareSize}
             height={SquareSize}
             color="#101010"
             r={35}
           />
           {/* 👇 I'm the inner white light on top of the square (faking the light source 💡) */}
           <Shadow color="#4c4c4c" inner blur={0} dx={0} dy={0.8} />
           {/* 👇 I'm the animated shadow */}
           <Shadow color="#000000" blur={3.5} dx={shadowDx} dy={shadowDy} />
         </Group>
       </Canvas>
     </View>
   </View>
 );
};
// ... existing code 
```

Can you feel the difference?

[Video: Dancing shadows demo](https://youtu.be/8NSiNyHcFh4)



### 4. Adding the React Native logo

This is totally optional but I think it's a nice touch to add the React Native Logo in the center of the square. Kind of like a _"stamp"_ to make the app feel more complete and to **remember that you can achieve whatever you want with React Native**.

To create the logo, we'll use the _Oval_ component from React Native Skia and we'll do some transformations to rotate the ovals. The beauty of it is that we can easily apply a glowing effect with a Skia _BlurMask_.

Once the component is created, we can add it to our canvas and animate it.

```tsx
import { BlurMask, Group, Oval } from '@shopify/react-native-skia';
import { useMemo } from 'react';

const SIZE = 60;
const OVAL_HEIGHT_RATIO = 2.5;

type CanvasSize = {
  width: number;
  height: number;
};

type ReactLogoSkiaProps = {
  canvasSize: CanvasSize;
};

const OvalComponent = () => (
  <Oval
    x={-SIZE}
    y={-SIZE / OVAL_HEIGHT_RATIO}
    width={SIZE * 2}
    height={(SIZE / OVAL_HEIGHT_RATIO) * 2}
    color="#cecece"
    style="stroke"
    strokeWidth={2}
  />
);

export const ReactNativeLogo = ({ canvasSize }: ReactLogoSkiaProps) => {
  const rotatedOvals = useMemo(() => {
    const angles = [0, Math.PI / 3, (Math.PI * 2) / 3];
    return angles.map((angle, index) => (
      <Group key={index} transform={[{ rotate: angle }]}>
        <OvalComponent />
      </Group>
    ));
  }, []);

  return (
    <Group>
      <Group
        transform={[
          { translateX: canvasSize.width / 2 },
          { translateY: canvasSize.height / 2 },
        ]}
      >
        {rotatedOvals}
      </Group>
      <BlurMask blur={3} style="solid" />
    </Group>
  );
};
```

```tsx
// ... existing code 
 return (
   <View style={styles.fill}>
     {...} {/* The background RadialGradient */}
     <View style={styles.container}>
       <Canvas
         style={{
           height: CanvasSize.height,
           width: CanvasSize.width,
         }}>
         <Group origin={CanvasCenter} transform={rTransform}>
           <Group>
             {...} {/* The square with the shadow */}
           </Group>
           <Group>
           <ReactNativeLogo canvasSize={CanvasSize} /> {/* 👈 New addition */}
           </Group>
         </Group>
       </Canvas>
     </View>
   </View>
 );
};
// ... existing code 
```

So far, this is the result we've got:

![a work in progress...](https://cdn.sanity.io/images/9r24npb8/production/443806182294a912c584fcec78c3055b49d01989-2880x2160.jpg)

## The beauty always lies in the details

It seems like we're done, but there's always room for improvement. Can you spot the imperfections?

![The devil is in the details](https://cdn.sanity.io/images/9r24npb8/production/83f535144db50ea0c70d4ce7c042e2c2b64bcde1-3484x1863.png)

As you can hopefully see, it seems like the expected result has more depth. It just feels more "real".

To get this effect, we're going to add a subtle gradient on the square so that it looks like the light is shining through it.

First of all, let's extract our _RoundedRect_ into a separate component:

```tsx
// ... existing code 
// I guess this is a good name for it 🤷🏼
const GoodOldSquare = useCallback(
 ({ children }: { children?: React.ReactNode }) => {
   return (
     <RoundedRect
       x={CanvasSize.width / 2 - SquareSize / 2}
       y={CanvasSize.height / 2 - SquareSize / 2}
       width={SquareSize}
       height={SquareSize}
       color="#101010"
       r={35}>
       {children}
     </RoundedRect>
   );
 },
 [],
);
// ... existing code 
```

Then we can reuse it in our _Canvas_ and apply a _LinearGradient_ to the _GoodOldSquare_.

```tsx
// ... existing code 
 return (
   <View style={styles.fill}>
     {...} {/* The background RadialGradient */}
     <View style={styles.container}>
       <Canvas
         style={{
           height: CanvasSize.height,
           width: CanvasSize.width,
         }}>
         <Group origin={CanvasCenter} transform={rTransform}>
           <Group>
             {/* The base square */}
             <GoodOldSquare />
             {/* The square with the light shining through */}
             <GoodOldSquare>
               <LinearGradient
                 start={vec(0, 0)}
                 end={vec(0, CanvasSize.height / 1.8)}
                 colors={['#2e2e2e', '#0e0e0e']}
               />
               {/* Blurring the linear gradient is always the secret sauce */}
               <Blur blur={10} />
             </GoodOldSquare>
             <Shadow color="#4c4c4c" inner blur={0} dx={0} dy={0.8} />
             <Shadow color="#000000" blur={3.5} dx={shadowDx} dy={shadowDy} />
           </Group>
           <Group>
             {...} {/* The logo ⚛ */}
           </Group>
         </Group>
       </Canvas>
     </View>
   </View>
 );
};
// ... existing code 
```

And that's it, finally, our block of wood is alive!

## Conclusion

In this article, we've explored how to leverage device sensors to create engaging and interactive animations in React Native. We've taken a simple square and transformed it into a dynamic element that reacts to the device's orientation.

Here's what we've accomplished:

1. Capture device rotation and gravity data using Reanimated's _useAnimatedSensor_
2. Implement smooth animations for both X and Y-axis rotations
3. Add depth and realism to our animation with crafted shadows and lighting effects using React Native Skia

And just a reminder: we didn't only create a 3D animated square for iOS, we also built an Android version.

_Because that's what React Native has always been about._

[Video: We built two apps at once. ](https://youtu.be/XYlBrezqgJY)

