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.
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.
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.
React Native Skia offers two types of APIs to draw graphics:
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:
180
, to give the rectangle a height of 180 pixels300
, to give the rectangle a width of 300 pixelsX = 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 sectionY = center.y
: 90
, this is the position of the rectangle from the Y-axiscolor = #eee
: gives our rectangle a white colorrx = 12
: gives our rectangle a corner radius of 12
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.
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.
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:
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:
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 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.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
3 Replies to "Create high-performance graphics with React Native Skia"
You ought to compete with flutter, not become one
Thanks for this great summary. The package can now directly be installed from yarn/npm
Awesome 🔥