English | 한국어
A React Native library for creating inset shadows and reflected light effects with React Native Skia. Supports both solid and linear gradient backgrounds for advanced UI designs, plus interactive pressable or toggle states using Reanimated.
# Using npm
npm install react-native-inner-shadow @shopify/react-native-skia react-native-reanimated
# Or using Yarn
yarn add react-native-inner-shadow @shopify/react-native-skia react-native-reanimated
If you’re using Expo, you can run:
npx expo install react-native-inner-shadow @shopify/react-native-skia react-native-reanimated
Important You must have React Native Skia and Reanimated properly installed and configured in your React Native project. See the Skia documentation and the Reanimated installation guide for details.
Add react-native-reanimated/plugin plugin to your babel.config.js.
module.exports = {
presets: [
... // don't add it here :)
],
plugins: [
...
'react-native-reanimated/plugin',
],
};
Don't forget to run after installing the dependencies:
cd ios && bundle exec pod install && cd ..
- react-native-inner-shadow
![inner shadow & linear shadow sample](https://private-user-images.githubusercontent.com/77220824/406307237-c588c061-d2c3-4d90-85ed-c71689b2a8cf.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzNjkwMjAsIm5iZiI6MTczOTM2ODcyMCwicGF0aCI6Ii83NzIyMDgyNC80MDYzMDcyMzctYzU4OGMwNjEtZDJjMy00ZDkwLTg1ZWQtYzcxNjg5YjJhOGNmLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjEyVDEzNTg0MFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk5OGIxMDgyMTE3ZmVmNmQ3MGY4ZTE2Njc4ZjNiYWZlMjY3YjExOGYxOGI2OTU2MzA2YjRkMzZmN2Y3NWQ0YWEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.z7_8i0wdrxkyZyYLXwf-2pr7Z4hxDy5ti1FD_xXNlbM)
![inner shadow pressable & toggle sample gif](https://private-user-images.githubusercontent.com/77220824/406707485-2a59b1a8-65df-487b-aabe-1f0a94d5aa10.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzNjkwMjAsIm5iZiI6MTczOTM2ODcyMCwicGF0aCI6Ii83NzIyMDgyNC80MDY3MDc0ODUtMmE1OWIxYTgtNjVkZi00ODdiLWFhYmUtMWYwYTk0ZDVhYTEwLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjEyVDEzNTg0MFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTI4N2FmYWRjNjk0M2YyNTY3ZDZmODk4ZDdkMDUxYTkzZjk5OGI0YjA2NmQ0ZjEyZjg1ZjhlZWFhNGFkYWQ1Y2ImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.e_X1vXTWMSzyrXoVnaznjwRuQaeCgDNbrm6LHcjmeSQ)
- Inset Shadows: Achieve the “inset” effect that isn’t natively supported by React Native.
- Reflected Light: Optional highlight (on the opposite side of the main shadow) for a more 3D, “depressed” look.
- Solid or Linear Gradient: Choose between a plain background color (
InnerShadowView
) or multi-color gradient (LinearShadowView
). - Pressable & Toggle Support: Interactive variants (
ShadowPressable
,ShadowToggle
) to animate shadows with Reanimated. - High Performance: Powered by React Native Skia for smooth cross-platform rendering.
import React from 'react';
import {Text, View} from 'react-native';
import {ShadowView} from 'react-native-inner-shadow';
export default function App() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<ShadowView
style={{
width: 120,
height: 120,
borderRadius: 12,
backgroundColor: '#f0f0f0',
alignItems: 'center',
justifyContent: 'center',
}}
inset
shadowColor="#00000066"
shadowOffset={{width: 2, height: 2}}
shadowBlur={5}
isReflectedLightEnabled={false}>
<Text style={{textAlign: 'center', color: '#2f2f2f', fontSize: 14}}>
inner-shadow
</Text>
</ShadowView>
</View>
);
}
import React from 'react';
import { View } from 'react-native';
import { LinearShadowView } from 'react-native-inner-shadow';
export default function GradientExample() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<LinearShadowView
style={{ width: 150, height: 150, borderRadius: 16 }}
from="top"
to="bottom"
colors={['#FF7A7A', '#FFE08C']}
shadowColor="#22222299"
shadowOffset={{ width: 4, height: 4 }}
shadowBlur={8}
inset
>
{/* Your content */}
</LinearShadowView>
</View>
);
}
ShadowPressable
is a specialized component that provides a “press in, press out” shadow animation for a physically realistic button effect. By default, pressing it inverts the shadow (making it look “depressed”), then returns to the original raised state on release. This is powered by Skia (for drawing) and Reanimated (for animations).
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {ShadowPressable} from 'react-native-inner-shadow';
export default function App() {
return (
<View style={styles.container}>
<ShadowPressable
style={styles.button}
initialDepth={2}
shadowBlur={7}
duration={200}
damping={1.2}>
<Text style={styles.label}>Press Me</Text>
</ShadowPressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
width: 120,
height: 63,
backgroundColor: '#E5A9A9',
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
label: {
fontSize: 14,
fontWeight: 'bold',
color: '#F4E4BA',
},
});
Prop | Type | Default | Description |
---|---|---|---|
width |
number |
0 |
Manual width (optional; if style has a width, that’s used). |
height |
number |
0 |
Manual height (optional; if style has a height, that’s used). |
initialDepth |
number |
3 |
Sets how “raised” the shadow is initially. |
shadowSpace |
number |
9 |
Extra space inside the <Canvas> to prevent clipping if you have a big blur or offset. |
shadowBlur |
number |
20 |
Blur radius for the main shadow. Adjust to taste for softer/harder edges. |
shadowColor |
string |
'#2F2F2FBC' |
Color of the main shadow (can be semi-transparent). |
reflectedLightColor |
string |
'#EEE9E92D' |
The color for a highlight on the opposite side of the main shadow. |
duration |
number |
200 |
Animation duration (ms) for the press in/out transitions. |
damping |
number |
0.8 |
Scales how far the shadow insets when pressed. (Sometimes spelled “damping.”) |
isReflectedLightEnabled |
boolean |
true |
Whether to draw a second “reflected light” shadow. |
Behavior
- Press In: Moves the shadow to an inset state (
depth < 0
). - Release: Returns shadow to the original “raised” position.
- Clipping: If you do large offsets or big blurs, consider increasing
shadowSpace
.
ShadowToggle
is a controlled variant of ShadowPressable
(or a similar logic) that toggles based on an external isActive
prop. If isActive
is true
, it insets the shadow; if false
, it shows it raised. You can also interpolate background colors if you pass an activeColor
.
- You need a “toggle” or “switch” style button, where the pressed state is persistent, controlled by external logic.
- You want to highlight the toggle is “on” or “off” visually with a shadow inset and possibly a different background color.
import React, {useState} from 'react';
import {View, Text, StyleSheet} from 'react-native';
import {ShadowToggle} from 'react-native-inner-shadow';
export default function ToggleExample() {
const [isActive, setIsActive] = useState(false);
return (
<View style={styles.container}>
<ShadowToggle
initialDepth={3}
style={styles.toggle}
isActive={isActive}
activeColor="#FFD700"
onPress={() => setIsActive(prev => !prev)}>
<Text
style={[
styles.label,
{
color: isActive ? '#515050' : '#eeebeb',
},
]}>
{isActive ? 'ON' : 'OFF'}
</Text>
</ShadowToggle>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
toggle: {
width: '30%',
aspectRatio: 1.7,
borderRadius: 12,
backgroundColor: '#06d6a0',
justifyContent: 'center',
alignItems: 'center',
},
label: {
fontWeight: 'bold',
},
});
Prop | Type | Default | Description |
---|---|---|---|
isActive |
boolean |
false |
Whether the shadow is inset (active) or raised (inactive). |
activeColor |
string |
null |
If provided, transitions the background color when isActive is true . |
initialDepth |
number |
3 |
How “deep” the raised state is. Inset goes negative, so isActive toggles shadow inward. |
damping |
number |
0.8 |
Controls how far the shadow insets if isActive is true . |
shadowBlur |
number |
10 |
Blur radius for the main shadow. |
shadowSpace |
number |
6 |
Extra canvas space to avoid clipping. |
duration |
number |
200 |
Animation duration (ms) for toggling from inactive→active or vice versa. |
isReflectedLightEnabled |
boolean |
true |
Whether to draw the highlight reflection. |
isActive
→ Inset: The shadow shifts inward (negative depth).!isActive
→ Raised: The shadow returns to a normal outer/neutral depth.- Optional Color: Switch to
activeColor
ifisActive
istrue
; fallback to normalbackgroundColor
otherwise.
- Controlled vs. Uncontrolled: Typically, you store
isActive
in your state (likeuseState
), then pass it down. You can also define an internal state if you want a fully self-contained toggle. - Performance: If you have many toggles, test carefully on lower-end devices, though Skia + Reanimated is usually quite efficient.
This library offers multiple ways to create inset or togglable shadows:
ShadowView
- A simpler, “solid background” approach. Inherits from
ShadowViewProps
.
- A simpler, “solid background” approach. Inherits from
LinearShadowView
- Extends
InnerShadowProps
with gradient logic (from
,to
,colors
).
- Extends
ShadowPressable
- Pressable version with an animated “press in” effect.
ShadowToggle
- Controlled version toggling an inset shadow based on
isActive
.
- Controlled version toggling an inset shadow based on
Click to expand
export interface InnerShadowProps extends ViewProps {
children?: React.ReactNode;
inset?: boolean;
shadowColor?: string;
shadowOffset?: { width: number; height: number };
shadowBlur?: number;
isReflectedLightEnabled?: boolean;
reflectedLightColor?: string;
reflectedLightOffset?: { width: number; height: number };
reflectedLightBlur?: number;
width?: number;
height?: number;
backgroundColor?: string;
style?: ViewStyle;
}
- Defines basic shadow properties, optional reflected light, and optional sizing.
inset
: Whether the shadow is drawn inside (true
) or outside (false
).shadowBlur
: Typically 0–20 for a soft spread.style
: Accepts a typical RNViewStyle
; you can specifyborderRadius
for rounded corners.
Click to expand
export type LINEAR_DIRECTION = 'top' | 'bottom' | 'left' | 'right';
export interface LinearInnerShadowProps extends InnerShadowProps {
from?: LINEAR_DIRECTION;
to?: LINEAR_DIRECTION;
colors: AnimatedProp<Color[]>;
}
- Inherits all from
InnerShadowProps
plus gradient fields. from
andto
define the gradient’s start/end direction (liketop
→bottom
).colors
can be an array of color strings or an animated array (AnimatedProp<Color[]>
).
This library exports default constants for fallback colors, blur values, etc.:
Constant | Description |
---|---|
DEFAULT_SHADOW_OFFSET_SCALE |
Scale factor for default shadow offsets (e.g., 2 ). |
DEFAULT_REFLECTED_LIGHT_OFFSET_SCALE |
Scale factor for reflected light offsets (e.g., 2 ). |
DEFAULT_BACKGROUND_COLOR |
Default background color (#FFFFFF ). |
DEFAULT_REFLECTED_LIGHT_COLOR |
Default highlight color (#EEE9E92D ). |
DEFAULT_SHADOW_COLOR |
Default main shadow color (#2F2F2FBC ). |
DEFAULT_SHADOW_BLUR |
Default blur radius for the main shadow (3 ). |
DEFAULT_REFLECTED_LIGHT_BLUR |
Default blur radius for the reflected light (3 ). |
-
Reflected Light
- When
inset
istrue
,isReflectedLightEnabled
often defaults totrue
. AdjustreflectedLightColor
andreflectedLightOffset
for subtle or more dramatic highlights.
- When
-
Performance
- Each shadow component uses a Skia
<Canvas>
. For best results, avoid re-measuring every layout pass. You can specify a fixedwidth
andheight
if the layout is static.
- Each shadow component uses a Skia
-
Border Radii
- Only a single numeric
borderRadius
is fully supported. For advanced shapes or different corner radii, you’d need to draw custom paths with Skia.
- Only a single numeric
-
Testing
- If you have many shadows or toggles in a list, test on lower-end devices to ensure smooth performance. Skia + Reanimated is generally fast, but heavy usage might require memoization or other optimizations.
-
Version Conflicts
- Because we rely on Skia and Reanimated, ensure your versions match the React Native environment. If you run into errors like “react-native-reanimated is not installed!”, move them to your project’s
peerDependencies
and install them at the root.
- Because we rely on Skia and Reanimated, ensure your versions match the React Native environment. If you run into errors like “react-native-reanimated is not installed!”, move them to your project’s
We hope you enjoy building immersive, 3D-like UI components with react-native-inner-shadow
. If you have suggestions, bug reports, or want to contribute, please open an issue or create a pull request!