├── .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 | [](https://npm.im/react-native-figma-squircle) [](./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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------