Written by Rupesh Chaudhari✏️
Skia is a popular open-source 2D graphics library used by major platforms like Google Chrome, Chrome OS, Android, and Flutter as their default graphics engine. The library is sponsored and managed by Google, while the development is overseen by Skia’s engineering team.
What is React Native Skia?
Thanks to Shopify, William Candollin, Christian Falch, and the entire dev team behind react-native-skia, we can now use Skia in our React Native applications to draw awesome graphics and create trendy UI concepts like Neumorphism
and Glassmorphism
.
Note: The library is still in alpha stage and is not yet available on npm, thus, it is not completely stable yet.
You can find the complete code for this article in this GitHub repository.
Installing React Native Skia
Let’s get started with library installation in a bare React Native project. Because the library is not yet published on npm, we need to download it from their GitHub repository.
Let’s start by initializing a new react native project.
react-native init <project-name>
After we set our project up, let’s add React Native Skia.
Run the following in your terminal:
yarn add https://github.com/Shopify/react-native-skia/releases/download/v0.1.106-alpha/shopify-react-native-skia-0.1.106.tgz
Or, if you use npm:
npm install https://github.com/Shopify/react-native-skia/releases/download/v0.1.106-alpha/shopify-react-native-skia-0.1.106.tgz
At the time of writing, the latest alpha release version is 0.1.106
, which we’ll use in this tutorial.
Note: once newer versions are released, there will likely be some changes and new features.
Once the package is downloaded and installed, we need to set up the project in our native iOS and Android projects.
For iOS, this is pretty straightforward — we just need to install the cocoa pods dependency. Use this command to do so:
cd ios
pod install
Now, we need to rebuild our iOS project so that we can use Skia
in our React Native iOS projects.
Build the project from the terminal or directly from Xcode:
yarn ios
// OR
npm run ios
Setting up our project in Android can be a bit complex if you’re not familiar with the environment. Because most of the graphic rendering logic for React Native Skia is written in C++
, we will need something known as NDK
(Android Native Development Kit) for the communication between Java code and C++ code.
“The Native Development Kit (NDK) is a set of tools that allows you to use C and C++ code with Android” - Android Documentation
To install Android NDK, you can follow these steps from the official Android docs. Now that we set our library up in both iOS and Android, we can now begin creating a beautiful user interface and graphics.
Using React Native Skia in a React Native app
React Native Skia offers two types of APIs to draw graphics:
- A declarative API, which uses the default React Native renderer
- An imperative API, which works with JSI (JavaScript Interface)
React Native JSI is a core change in the re-architecture of React Native. This is a layer that offers “synchronous” communication between JavaScript and native code. It will be replacing the default react native bridge. (Source)
The declarative API is similar to how we currently create UI in React Native using JSX syntax. If we want to create a rectangular shape with rounded corners, then the syntax for it will look like this:
import React from 'react';
import { Canvas, Fill, RoundedRect, vec } from '@shopify/react-native-skia';
import { StatusBar, useWindowDimensions } from 'react-native';
export const DeclarativeAPI = () => {
const {width, height} = useWindowDimensions();
const center = vec(width / 2, height / 2);
return (
<>
<StatusBar barStyle="light-content" />
<Canvas style={{flex: 1}}>
<Fill color={'#222'} />
<RoundedRect
height={180}
width={300}
x={center.x - 150}
y={center.y - 90}
color={'#eee'}
rx={12}
/>
</Canvas>
</>
);
};
The output from the above code will be:
The library owners prefer users to use the declarative API, as it is easy to use and the code is readable.
Here’s what we have done in the above code. First, we needed a canvas to draw graphics on, so we wrapped our components with a Canvas
component and gave it a style of flex:1
so that it takes up the entire screen space. Then, we used the Fill
component to fill our Canvas
with color #222
.
We also declared a variable center
, which is vector
with two keys, X and Y. Finally, to draw our Rectangle
shape with rounded corners, we used the RoundedRect
component and gave it below props:
- Height:
180
, to give the rectangle a height of 180 pixels - Width:
300
, to give the rectangle a width of 300 pixels -
X = center.x
:150
, which is the position of the rectangle from the X-axis. In React Native Skia, the X and Y positions start from the top left section -
Y = center.y
:90
, this is the position of the rectangle from the Y-axis -
color = #eee
: gives our rectangle a white color -
rx = 12
: gives our rectangle a corner radius of12
Now, let’s see how we can create the same UI using the imperative API, which looks like this:
import React from 'react';
import {
rect, rrect, Skia,
SkiaView,
useDrawCallback, vec
} from '@shopify/react-native-skia';
import { StatusBar, useWindowDimensions } from 'react-native';
const paint = Skia.Paint();
paint.setAntiAlias(true);
export const ImperativeAPI = () => {
const {width, height} = useWindowDimensions();
const center = vec(width / 2, height / 2);
const rRect = rrect(rect(center.x - 150, center.y - 90, 300, 180), 12, 12);
const onDraw = useDrawCallback(canvas => {
const white = paint.copy();
white.setColor(Skia.Color('#eee'));
canvas.drawRRect(rRect, white);
}, []);
return (
<>
<StatusBar barStyle="light-content" />
<SkiaView style={{flex: 1, backgroundColor: '#222'}} onDraw={onDraw} />
</>
);
};
The output for the above is identical to the one created by the declarative API.
Here, we created a SkiaView
, which is essentially our Canvas
, and we are using its onDraw
callback to define what to draw on the view. Then, there is the onDraw
method, which uses the useDrawCallback
hook and returns a memoized callback. In the function, we are initializing Paint
, then using that paint as the color of our rounded rectangle that we drew on the canvas.
Adding filters to images in React Native Skia
Now, let’s see how you can display an image using Skia’s Image
component, as well as how to add cool filters to the image in real time.
Let’s write code to display the image first.
import {Canvas, Image, useImage} from '@shopify/react-native-skia';
import React from 'react';
import {Dimensions, SafeAreaView, StyleSheet, Text} from 'react-native';
const {height, width} = Dimensions.get('window');
export const ImageFilters = () => {
const image = useImage(require('../assets/image.jpg'));
if (image === null) return null;
return (
<SafeAreaView>
<Text style={styles.title}>Image Filters</Text>
<Canvas style={styles.canvas}>
<Image
x={0}
y={0}
width={width - 32}
height={height - 300}
image={image}
fit="cover"
/>
</Canvas>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
title: {
paddingVertical: 16,
paddingHorizontal: 16,
fontWeight: '700',
fontSize: 32,
letterSpacing: 1.4,
},
canvas: {
height: height - 300,
marginHorizontal: 16,
width: width - 32,
},
});
In the above code, we initialize the image
variable using the useImage
hook from react-native-skia
.
We can use this hook to initialize local image assets with the help of require
helper function, or we can also use remote images from the network, like this:
const image = useImage('https://picsum.photos/1920/1080');
Then, we wrap the Image
component from Skia
in a Canvas
and add styles to the components. The output of the above code will be:
Now that our image is visible, let’s add some filters to it using the color matrix
, which is a matrix (2D-Array) with values for RGBA colors. The syntax is something like this:
| R' | | a00 a01 a02 a03 a04 | | R |
| G' | | a10 a11 a22 a33 a44 | | G |
| B' | = | a20 a21 a22 a33 a44 | * | B |
| A' | | a30 a31 a22 a33 a44 | | A |
We will use the ColorMatrix
filter component from React Native Skia to achieve this.
First, let’s create matrices for all the filters we will be using:
const filters = {
Juno: [
1, 0, 0, 0, 0,
-0.4, 1.3, -0.4, 0.2, -0.1,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
],
Sepia: [
0.393, 0.769, 0.189, 0, 0,
0.349, 0.686, 0.168, 0, 0,
0.272, 0.534, 0.131, 0, 0,
0, 0, 0, 1, 0,
],
Greyscale: [
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 1, 0,
],
Gingham: [
2, 0, 0, 0, 0,
1, 1, 0, 0, 0,
0.5, 0, 1, 0, 0,
0, 0, 0, 1, 0,
],
Mayfair: [
1, 1, 0.5, 0, 0,
0, 0.5, 1, 0, 0,
0.5, 0.5, 1, 0, 0,
0, 0, 0, 1, 0,
],
Valencia: [
1, 0, 0, 0, 0,
-0.2, 1, 0, 0, 0,
-0.8, 1.6, 1, 0, 0,
0, 0, 0, 1, 0,
],
'No Filter': [
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
]
};
You can also create your own custom color matrix, there are many playgrounds available. Note that we created an object with seven keys, each pointing to a ColorMatrix
value.
After adding some code to handle our UI, the final code for this will be:
import {
Canvas,
ColorMatrix,
Image,
Paint,
useImage,
} from '@shopify/react-native-skia';
import React, {useCallback, useState} from 'react';
import {
Dimensions,
FlatList,
SafeAreaView,
StyleSheet,
Text,
} from 'react-native';
const {height, width} = Dimensions.get('window');
const filters = {
Juno : [
1, 0, 0, 0, 0,
-0.4, 1.3, -0.4, 0.2, -0.1,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
],
Sepia: [
0.393, 0.769, 0.189, 0, 0,
0.349, 0.686, 0.168, 0, 0,
0.272, 0.534, 0.131, 0, 0,
0, 0, 0, 1, 0,
],
Greyscale: [
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 1, 0,
],
Gingham: [
2, 0, 0, 0, 0,
1, 1, 0, 0, 0,
0.5, 0, 1, 0, 0,
0, 0, 0, 1, 0,
],
Mayfair: [
1, 1, 0.5, 0, 0,
0, 0.5, 1, 0, 0,
0.5, 0.5, 1, 0, 0,
0, 0, 0, 1, 0,
],
Valencia: [
1, 0, 0, 0, 0,
-0.2, 1, 0, 0, 0,
-0.8, 1.6, 1, 0, 0,
0, 0, 0, 1, 0,
],
['No Filter']: [
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
]
};
export const ImageFilters = () => {
const [selectedFilter, setSelectedFilter] = useState('Juno');
const handlePress = useCallback(
item => () => {
setSelectedFilter(item);
},
[],
);
const image = useImage(require('../assets/image.jpg'));
if (image === null) return null;
return (
<SafeAreaView>
<Text style={styles.title}>Image Filters</Text>
<Canvas style={styles.canvas}>
<Paint>
<ColorMatrix
matrix={filters[selectedFilter]}
/>
</Paint>
<Image
x={0}
y={0}
width={width - 32}
height={height - 300}
image={image}
fit="cover"
/>
</Canvas>
<FlatList
numColumns={3}
data={Object.keys(filters)}
keyExtractor={(_, index) => index}
renderItem={({item}) => (
<Text
style={[
styles.item,
selectedFilter === item && styles.selectedItem,
]}
onPress={handlePress(item)}>
{item}
</Text>
)}
style={styles.list}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
title: {
paddingVertical: 16,
paddingHorizontal: 16,
fontWeight: '700',
fontSize: 32,
letterSpacing: 1.4,
color: '#000',
},
canvas: {
height: height - 300,
marginHorizontal: 16,
width: width - 32,
borderRadius: 12,
},
list: {
margin: 16,
},
item: {
width: '33%',
textAlign: 'center',
marginBottom: 12,
fontWeight: '600',
fontSize: 18,
},
selectedItem: {
color: '#ea4c89',
borderWidth: 1,
borderColor: '#ea4c89',
borderRadius: 12,
},
});
The output from the above code in iOS and Android are:
That’s it! We’ve now added filters to our images in React Native.
Creating neumorphic UI with React Native Skia
With Skia
, we can create neumorphic UI in React Native. React Native Skia provides a DropShadow
component for creating shadows, similar to how we would in web apps to create a neumorphic design pattern.
Below is the code to create a neumorphic list:
import {
Canvas,
DropShadow,
Group,
Paint,
RoundedRect,
Text,
useFont,
} from '@shopify/react-native-skia';
import React from 'react';
import {Dimensions, ScrollView} from 'react-native';
const data = [
{
id: 1,
name: 'John Doe',
body: 'Sunt aliqua eu officia eiusmod non',
},
{
id: 2,
name: 'Jane Doe',
body: 'Sunt aliqua eu officia eiusmod non',
},
{
id: 3,
name: 'Erin Yeager',
body: 'Adipisicing sint ullamco irure nulla',
},
{
id: 4,
name: 'Mikasa Ackerman',
body: 'Adipisicing sint ullamco irure nulla',
},
{
id: 5,
name: 'John Doe',
body: 'Sunt aliqua eu officia eiusmod non',
},
{
id: 6,
name: 'Jane Doe',
body: 'Sunt aliqua eu officia eiusmod non',
},
{
id: 7,
name: 'John Doe',
body: 'Sunt aliqua eu officia eiusmod non',
},
{
id: 8,
name: 'Jane Doe',
body: 'Sunt aliqua eu officia eiusmod non',
},
];
const {width} = Dimensions.get('window');
const NeumorphicCard = ({data, index, font, bodyFont}) => {
return (
<Canvas style={{height: 140, width}} key={index}>
<Group>
<Paint>
<DropShadow dx={6} dy={6} blur={4} color="rgba(0, 0, 0, 0.4)" />
<DropShadow dx={-6} dy={-6} blur={4} color="rgba(255, 255, 255, 1)" />
</Paint>
<RoundedRect
x={16}
y={20}
width={width - 32}
height={100}
rx={12}
color="#EFEEEE">
<Paint
color="rgba(255, 255, 255, 0.6)"
style="stroke"
strokeWidth={1}
/>
</RoundedRect>
</Group>
<Text x={32} y={56} text={data?.name} font={font} />
<Text x={32} y={92} text={data?.body} font={bodyFont} />
</Canvas>
);
};
export const Neumorphism = () => {
const font = useFont(require('../fonts/Poppins-Regular.ttf'), 20);
const bodyFont = useFont(require('../fonts/Poppins-Regular.ttf'), 14);
if (font === null || bodyFont === null) {
return null;
}
return (
<ScrollView
showsVerticalScrollIndicator={false}
style={{flex: 1, backgroundColor: '#EFEEEE'}}
contentContainerStyle={{
paddingTop: Platform.OS === 'ios' ? 40 : 0,
paddingBottom: 40,
}}>
{data.map(item => (
<NeumorphicCard
key={item.id}
data={item}
font={font}
bodyFont={bodyFont}
/>
))}
</ScrollView>
);
};
Above, we are mapping through an array of random data, and, for each item, we are rendering a NeumorphicCard
component. We are also using the useFont
hook to load custom fonts.
In the card component, we have a RoundedRect
with a border of 1
unit, which we have created using Paint
with the style of stroke
. Then, we added two DropShadow
components to create a shadow on the top-left and bottom-right side of the rectangle.
Here is the output of the above code in iOS and Android:
Incorporating glassmorphism with React Native Skia.
Finally, let’s create a glassmorphism-inspired design in our React Native app. To create this glassmorphic design, we will need the BackDropBlur
component. We will create a RoundedRect
, then give the background a blur
using the BackDropBlur
component. We will also see how we can animate graphics.
Write the below code to create an animated glassmorphic card:
import {
BackdropBlur, Canvas, Fill, Group, Image, mix,
Paint, rect, Rect, RoundedRect, rrect, Text,
useDerivedValue, useFont, useImage, useLoop, vec,
} from '@shopify/react-native-skia';
import React from 'react';
import {useWindowDimensions} from 'react-native';
export const Glassmorphism = () => {
const {width, height} = useWindowDimensions();
const center = vec(width / 2 - 50, height / 2 - 100);
const blurClipPath = rrect(rect(24, center.y, width - 48, 200), 12, 12);
const image = useImage('https://picsum.photos/1920/1080');
const blurProgress = useLoop({duration: 2000});
const blur = useDerivedValue(
progress => mix(progress, 0, 10),
[blurProgress],
);
const font = useFont(require('../fonts/Poppins-Regular.ttf'), 40);
if (font === null) {
return null;
}
return (
<Canvas style={{flex: 1}}>
<Group>
<Rect x={0} y={0} width={width} height={height} />
<Image
x={0}
y={0}
width={width}
height={height}
image={image}
fit={'cover'}
/>
</Group>
<Group>
<RoundedRect
x={24}
y={center.y}
width={width - 48}
height={200}
color="#0000"
rx={12}>
<Paint
color="rgba(255, 255, 255, 0.8)"
style="stroke"
strokeWidth={1}
/>
</RoundedRect>
<BackdropBlur blur={blur} clip={blurClipPath}>
<Fill color={'rgba(122, 122, 122, 0.2)'} />
</BackdropBlur>
<Text
x={center.x - 50}
y={center.y + 110}
text="Hello Skia"
font={font}
color="#fff"
style="stroke"
/>
</Group>
</Canvas>
);
};
The output will look like this:
Conclusion
Now you’ve seen just how powerful React Native Skia is, and I think it will change how we create UI in React Native. Because the library uses the full potential of the new React Native architecture (JSI) and the new renderer, it will surely increase our React Native application’s performance. Thanks for reading!
LogRocket: Instantly recreate issues in your React Native apps.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Top comments (0)