├── .eslintignore ├── .eslintrc.json ├── squircle.jpg ├── example ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ └── adaptive-icon.png ├── tsconfig.json ├── .gitignore ├── babel.config.js ├── metro.config.js ├── package.json ├── app.json └── App.tsx ├── .prettierrc.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── src └── index.tsx /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["expo", "eslint:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /squircle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-figma-squircle/HEAD/squircle.jpg -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-figma-squircle/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-figma-squircle/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-figma-squircle/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phamfoo/react-native-figma-squircle/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "trailingComma": "es5", 5 | "useTabs": false, 6 | "semi": false, 7 | "quoteProps": "consistent" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Node 5 | node_modules/ 6 | npm-debug.log 7 | yarn-debug.log 8 | yarn-error.log 9 | 10 | # Expo 11 | .expo/ 12 | 13 | # Build 14 | dist/ 15 | lib/ 16 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "react-native-figma-squircle": ["../src/index"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getConfig } = require('react-native-builder-bob/babel-config'); 3 | const pkg = require('../package.json'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | module.exports = function (api) { 8 | api.cache(true); 9 | 10 | return getConfig( 11 | { 12 | presets: ['babel-preset-expo'], 13 | }, 14 | { root, pkg } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getDefaultConfig } = require('@expo/metro-config'); 3 | const { getConfig } = require('react-native-builder-bob/metro-config'); 4 | const pkg = require('../package.json'); 5 | 6 | const root = path.resolve(__dirname, '..'); 7 | 8 | /** 9 | * Metro configuration 10 | * https://facebook.github.io/metro/docs/configuration 11 | * 12 | * @type {import('metro-config').MetroConfig} 13 | */ 14 | module.exports = getConfig(getDefaultConfig(__dirname), { 15 | root, 16 | pkg, 17 | project: __dirname, 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "allowUnusedLabels": false, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "lib": ["esnext"], 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noImplicitUseStrict": false, 14 | "noStrictGenericChecks": false, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "target": "esnext" 21 | }, 22 | "include": ["src"] 23 | } 24 | 25 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo/metro-runtime": "~4.0.1", 13 | "expo": "~52.0.37", 14 | "expo-status-bar": "~2.0.1", 15 | "react": "18.3.1", 16 | "react-dom": "18.3.1", 17 | "react-native": "0.76.7", 18 | "react-native-web": "~0.19.13", 19 | "react-native-svg": "15.8.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.20.0", 23 | "react-native-builder-bob": "^0.36.0" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "newArchEnabled": true, 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": [ 16 | "**/*" 17 | ], 18 | "ios": { 19 | "supportsTablet": true 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | } 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tien Pham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-figma-squircle", 3 | "description": "Figma-flavored squircles for React Native", 4 | "author": "Tien Pham", 5 | "version": "0.4.0", 6 | "license": "MIT", 7 | "main": "lib/commonjs/index.js", 8 | "react-native": "src/index.tsx", 9 | "source": "src/index.tsx", 10 | "module": "lib/module/index.js", 11 | "types": "lib/typescript/index.d.ts", 12 | "scripts": { 13 | "prepare": "bob build", 14 | "tsc": "tsc --noEmit", 15 | "lint": "eslint src --ext ts,tsx" 16 | }, 17 | "peerDependencies": { 18 | "react": "*", 19 | "react-native": "*", 20 | "react-native-svg": "*" 21 | }, 22 | "dependencies": { 23 | "figma-squircle": "^1.1.0" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "~18.2.6", 27 | "babel-plugin-module-resolver": "^4.1.0", 28 | "eslint": "^8.51.0", 29 | "eslint-config-expo": "^8.0.1", 30 | "prettier": "^3.3.3", 31 | "react": "18.3.1", 32 | "react-native": "0.76.7", 33 | "react-native-builder-bob": "^0.37.0", 34 | "react-native-svg": "12.1.0", 35 | "typescript": "^5.5.4" 36 | }, 37 | "keywords": [ 38 | "squircle", 39 | "react", 40 | "react-native", 41 | "figma" 42 | ], 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/phamfoo/react-native-figma-squircle.git" 46 | }, 47 | "files": [ 48 | "src", 49 | "lib", 50 | "!**/__tests__", 51 | "!**/__fixtures__", 52 | "!**/__mocks__" 53 | ], 54 | "react-native-builder-bob": { 55 | "source": "src", 56 | "output": "lib", 57 | "targets": [ 58 | "commonjs", 59 | "module", 60 | "typescript" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react' 2 | import { StatusBar } from 'expo-status-bar' 3 | import { View, Text, Pressable } from 'react-native' 4 | import { SquircleView } from 'react-native-figma-squircle' 5 | 6 | export default function App() { 7 | return ( 8 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | {({ pressed }) => { 38 | return ( 39 | 47 | Button 48 | 49 | ) 50 | }} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 66 | 67 | 68 | 69 | 70 | {({ pressed }) => { 71 | return ( 72 | 83 | Button 84 | 85 | ) 86 | }} 87 | 88 | 89 | 90 | 91 | ) 92 | } 93 | 94 | function ContentColumn({ children }: PropsWithChildren<{}>) { 95 | return ( 96 | 103 | {children} 104 | 105 | ) 106 | } 107 | 108 | function Spacer() { 109 | return 110 | } 111 | 112 | function Label({ children }: PropsWithChildren<{}>) { 113 | return ( 114 | 122 | {children} 123 | 124 | ) 125 | } 126 | 127 | function ButtonText({ children }: PropsWithChildren<{}>) { 128 | return ( 129 | 135 | {children} 136 | 137 | ) 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Figma Squircle 2 | 3 | [![Stable Release](https://img.shields.io/npm/v/react-native-figma-squircle)](https://npm.im/react-native-figma-squircle) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE) 4 | 5 | > Figma-flavored squircles for React Native 6 | 7 | ## Disclaimer 8 | 9 | > This library is not an official product from the Figma team and does not guarantee to produce the same results as you would get in Figma. 10 | 11 | ## What is this? 12 | 13 | Figma has a great feature called [corner smoothing](https://help.figma.com/hc/en-us/articles/360050986854-Adjust-corner-radius-and-smoothing), allowing you to create rounded shapes with a seamless continuous curve (squircles). 14 | 15 | ![](squircle.jpg) 16 | 17 | This library helps you bring those squircles to your React Native apps. 18 | 19 | ## Before you install 20 | 21 | This library is a very light abstraction on top of [figma-squircle](https://github.com/tienphaw/figma-squircle). We also depend on [react-native-svg](https://github.com/react-native-svg/react-native-svg) to draw the SVG background. In many cases, it's a better idea to just use [figma-squircle](https://github.com/tienphaw/figma-squircle) directly: 22 | 23 | - You can use [react-native-skia](https://shopify.github.io/react-native-skia/docs/shapes/path) instead of `react-native-svg`. 24 | - More control and flexibility. For example, clipping can be done very easily using [Clip Path](https://shopify.github.io/react-native-skia/docs/group/#clip-path). 25 | 26 | ## Installation 27 | 28 | Install [react-native-svg](https://github.com/software-mansion/react-native-svg) 29 | 30 | Install this library: 31 | 32 | ```sh 33 | npm install react-native-figma-squircle 34 | ``` 35 | 36 | Make sure [the New Architecture](https://reactnative.dev/architecture/landing-page) is enabled. 37 | ## Usage 38 | 39 | A `SquircleView` can be used just like a normal `View`, except the background is rendered separately from the view background. So to change how it looks, you'll have to use the `squircleParams` prop instead of the `style` prop. 40 | 41 | ```jsx 42 | import { SquircleView } from 'react-native-figma-squircle' 43 | 44 | function PinkSquircle() { 45 | return ( 46 | 54 | ) 55 | } 56 | ``` 57 | 58 | ## Props 59 | 60 | Inherits [View Props](https://facebook.github.io/react-native/docs/view#props) 61 | 62 | ### squircleParams 63 | 64 | #### cornerSmoothing 65 | 66 | > `number` | **Required** 67 | 68 | Goes from 0 to 1, controls how smooth the corners should be. 69 | 70 | #### cornerRadius 71 | 72 | > `number` | defaults to `0` 73 | 74 | #### topLeftCornerRadius 75 | 76 | > `number` 77 | 78 | #### topRightCornerRadius 79 | 80 | > `number` 81 | 82 | #### bottomRightCornerRadius 83 | 84 | > `number` 85 | 86 | #### bottomLeftCornerRadius 87 | 88 | > `number` 89 | 90 | #### fillColor 91 | 92 | > `Color` | defaults to `#000` 93 | 94 | Similar to `backgroundColor` in the `style` prop. 95 | 96 | #### strokeColor 97 | 98 | > `Color` | defaults to `#000` 99 | 100 | Similar to `borderColor` in the `style` prop. 101 | 102 | #### strokeWidth 103 | 104 | > `number` | defaults to `0` 105 | 106 | Similar to `borderWidth` in the `style` prop. 107 | 108 | ## Thanks 109 | 110 | - Figma team for publishing [this article](https://www.figma.com/blog/desperately-seeking-squircles/) and [MartinRGB](https://github.com/MartinRGB) for [figuring out all the math](https://github.com/MartinRGB/Figma_Squircles_Approximation) behind it. 111 | - [George Francis](https://github.com/georgedoescode) for creating [Squircley](https://squircley.app/), which was my introduction to squircles. 112 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ViewProps, View, StyleSheet, Platform } from 'react-native' 3 | import { 4 | PropsWithChildren, 5 | ReactNode, 6 | useState, 7 | useRef, 8 | useLayoutEffect, 9 | } from 'react' 10 | import Svg, { Color, Path } from 'react-native-svg' 11 | import { getSvgPath } from 'figma-squircle' 12 | 13 | interface SquircleParams { 14 | cornerRadius?: number 15 | topLeftCornerRadius?: number 16 | topRightCornerRadius?: number 17 | bottomRightCornerRadius?: number 18 | bottomLeftCornerRadius?: number 19 | cornerSmoothing: number 20 | fillColor?: Color 21 | strokeColor?: Color 22 | strokeWidth?: number 23 | } 24 | 25 | interface SquircleViewProps extends ViewProps { 26 | squircleParams: SquircleParams 27 | } 28 | 29 | function SquircleView({ 30 | squircleParams, 31 | children, 32 | ...rest 33 | }: PropsWithChildren) { 34 | return ( 35 | 36 | 37 | {children} 38 | 39 | ) 40 | } 41 | 42 | function SquircleBackground({ 43 | cornerRadius = 0, 44 | topLeftCornerRadius, 45 | topRightCornerRadius, 46 | bottomRightCornerRadius, 47 | bottomLeftCornerRadius, 48 | cornerSmoothing, 49 | fillColor = '#000', 50 | strokeColor = '#000', 51 | strokeWidth = 0, 52 | }: SquircleParams) { 53 | return ( 54 | 55 | {({ width, height }) => { 56 | const hasStroke = strokeWidth > 0 57 | 58 | if (!hasStroke) { 59 | const squirclePath = getSvgPath({ 60 | width, 61 | height, 62 | cornerSmoothing, 63 | cornerRadius, 64 | topLeftCornerRadius, 65 | topRightCornerRadius, 66 | bottomRightCornerRadius, 67 | bottomLeftCornerRadius, 68 | }) 69 | 70 | return ( 71 | 72 | 73 | 74 | ) 75 | } else { 76 | const cornerRadii = [ 77 | cornerRadius, 78 | topLeftCornerRadius, 79 | topRightCornerRadius, 80 | bottomLeftCornerRadius, 81 | bottomRightCornerRadius, 82 | ].filter( 83 | (cornerRadius) => cornerRadius && cornerRadius > 0 84 | ) as number[] 85 | 86 | const maxStrokeWidth = Math.min(...cornerRadii) 87 | strokeWidth = Math.min(strokeWidth, maxStrokeWidth) 88 | const insetAmount = strokeWidth / 2 89 | 90 | const insetSquirclePath = getSvgPath({ 91 | width: width - strokeWidth, 92 | height: height - strokeWidth, 93 | cornerSmoothing, 94 | cornerRadius: getInnerRadius(cornerRadius, insetAmount), 95 | topLeftCornerRadius: getInnerRadius( 96 | topLeftCornerRadius, 97 | insetAmount 98 | ), 99 | topRightCornerRadius: getInnerRadius( 100 | topRightCornerRadius, 101 | insetAmount 102 | ), 103 | bottomRightCornerRadius: getInnerRadius( 104 | bottomRightCornerRadius, 105 | insetAmount 106 | ), 107 | bottomLeftCornerRadius: getInnerRadius( 108 | bottomLeftCornerRadius, 109 | insetAmount 110 | ), 111 | }) 112 | 113 | return ( 114 | 115 | 122 | 123 | ) 124 | } 125 | }} 126 | 127 | ) 128 | } 129 | 130 | function getInnerRadius(radius: number | undefined, insetAmount: number) { 131 | if (radius) { 132 | return Math.max(0, radius - insetAmount) 133 | } 134 | 135 | return radius 136 | } 137 | 138 | // Inspired by https://reach.tech/rect/ 139 | interface RectProps extends Omit { 140 | children: (rect: { width: number; height: number }) => ReactNode 141 | } 142 | 143 | function Rect({ children, ...rest }: RectProps) { 144 | const [rect, setRect] = useState<{ width: number; height: number } | null>( 145 | null 146 | ) 147 | const ref = useRef(null) 148 | 149 | useLayoutEffect(() => { 150 | if (!isSyncLayoutAccessAvailable()) { 151 | throw new Error("This library requires React Native's new architecture.") 152 | } 153 | 154 | // TODO: Maybe use `getBoundingClientRect` instead when it's stable https://gist.github.com/lunaleaps/148756563999c83220887757f2e549a3#file-tooltip-uselayouteffect-js-L77 155 | // From my testing, `measureInWindow` is still faster than `unstable_getBoundingClientRect` 156 | ref.current?.measureInWindow((_x, _y, width, height) => { 157 | setRect({ width, height }) 158 | }) 159 | }, []) 160 | 161 | return ( 162 | 163 | {rect ? children(rect) : null} 164 | 165 | ) 166 | } 167 | 168 | function isSyncLayoutAccessAvailable() { 169 | if (Platform.OS === 'web') { 170 | return true 171 | } 172 | 173 | return (globalThis as any).RN$Bridgeless === true 174 | } 175 | 176 | export { SquircleView, getSvgPath } 177 | export type { SquircleParams, SquircleViewProps } 178 | --------------------------------------------------------------------------------