react-native-continuous-corners
v2.0.0
Published
iOS-style continuous corner radius for React Native - Cross-platform support for both iOS and Android | iOS 风格连续圆角 - 支持 iOS 和 Android 双平台
Maintainers
Readme
📐 React Native Continuous Corners
✨ iOS-style Continuous Corners - Cross-platform support for iOS and Android
iOS-style continuous corner radius for React Native - Cross-platform support for both iOS and Android
🎯 Perfect Cross-platform Continuous Corner Solution
In React Native, Android does not natively support continuous corners for images (iOS's CALayer.cornerCurve = .continuous). Even with borderCurve="continuous", image corners on Android fall back to standard circular arcs, causing visual inconsistencies between iOS and Android.
This library solves the problem perfectly!
- iOS: Uses native
borderCurve='continuous'(zero overhead) - Android: Uses Skia Canvas inverted path corner filling (GPU accelerated)
Achieves perfectly consistent continuous corner effects for images on both Android and iOS.
💡 Solution
A high-performance React Native component that brings iOS continuous corner radius to both platforms. Uses native iOS borderCurve and Skia Canvas on Android for perfect cross-platform visual consistency with GPU acceleration.
✨ Features
📷 Image Component (ContinuousCornerImageRect) 🆕 Recommended
- 🖼️ Rectangular Image Continuous Corners: Dynamic width/height image component (Discovery cards, photo galleries, etc.)
- 🍎 iOS Native: Uses native
borderCurve='continuous'(zero overhead) - 🤖 Android Perfect: Uses Skia inverted path corner filling (Solution B)
- 🎬 Animation Support: Supports SharedValue animations (width, height, corner radius)
📷 Image Component (ContinuousCornerImage) - Backward Compatible
- 🖼️ Square Image Continuous Corners: Fixed-size square image component
- 🤖 Android Perfect Support: Uses Skia to implement iOS-style continuous corners on Android
- 🍎 iOS Native Effect: True iOS-style continuous corner radius
📦 Container Component (ContinuousCornerContainer) 🆕
- 🎴 Full Card Continuous Corners: Supports complex containers with photos + text + buttons
- 🍎 iOS Zero Overhead: iOS automatically uses native
borderCurve='continuous' - 🤖 Android Perfect Support: Uses Skia Canvas clipping, visually identical effect
- 🃏 Use Cases: Tinder cards, Explore activity cards, user profile cards, etc.
🚀 General Features
- 📱 Cross-platform Consistency: Identical visual effects on Android and iOS, no differences
- 🚀 High Performance: GPU-accelerated rendering, 60fps+ smooth animations
- 🎬 Animation Support: Seamless React Native Reanimated integration for dynamic corner animations
- 🎨 Customizable: Adjustable corner smoothing (0.0 = standard, 0.6 = iOS continuous, 1.0 = maximum smoothness)
- 📦 Easy to Use: Install via npm, no copy-paste required
- 💪 TypeScript: Full type safety support
🔍 Preview
| Standard Border Radius | iOS Continuous Corner |
|------------------------|----------------------|
| |
|
⚡ Comparison: Why Do You Need This Library?
| Solution | Android Image Corners | iOS Image Corners | Performance | Notes |
|----------|----------------------|-------------------|-------------|-------|
| react-native-continuous-corners | ✅ Perfect support | ✅ Native support | ⭐⭐⭐⭐⭐ | Cross-platform solution! iOS native + Android GPU accelerated |
| React Native borderCurve: 'continuous' | ❌ Images fall back to circular | ✅ Smooth (containers only) | ⭐⭐⭐⭐⭐ | ⚠️ iOS only, containers only. Images not supported on Android |
| react-native-fast-squircle | ❌ Cannot clip images | ⚠️ Unstable | ⭐⭐⭐ | Android rendering limitations, does not support images |
| Image + borderRadius | ❌ Standard corners only | ❌ Standard corners only | ⭐⭐⭐⭐⭐ | No continuous corner effect |
🎮 Try It Out
Want to see it in action? Check out the Example App - a complete Expo project with 3 interactive tests:
- ✅ Static corner radius demonstration
- ✅ Smoothing comparison (0.0 vs 0.6 vs 1.0)
- ✅ Animated corner radius (Timing & Spring animations)
cd example-app
npm install
npx expo start📦 Installation
1. Install the package
# Using npm
npm install react-native-continuous-corners @shopify/react-native-skia react-native-reanimated react-native-worklets
# Using yarn
yarn add react-native-continuous-corners @shopify/react-native-skia react-native-reanimated react-native-worklets
# Using Expo (Recommended)
npx expo install react-native-continuous-corners @shopify/react-native-skia react-native-reanimated react-native-worklets2. Configure Babel
Add react-native-reanimated/plugin to your babel.config.js:
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'], // or 'module:metro-react-native-babel-preset'
plugins: [
'react-native-reanimated/plugin', // ⚠️ Must be last!
],
};
};3. Clear cache and restart
# For Expo
npx expo start -c
# For React Native CLI
npx react-native start --reset-cache🚀 Quick Start
📷 Rectangular Image Component (Recommended)
import { ContinuousCornerImageRect } from 'react-native-continuous-corners';
function DiscoveryCard() {
return (
<ContinuousCornerImageRect
source="https://example.com/photo.jpg"
width={screenWidth - 40}
height={500}
cornerRadius={24}
cornerSmoothing={0.6}
fit="cover"
containerBackgroundColor="transparent"
/>
);
}🎬 Animation Support (Rectangular Images)
import { ContinuousCornerImageRect } from 'react-native-continuous-corners';
import { useSharedValue, withTiming } from 'react-native-reanimated';
function AnimatedPhoto() {
const cornerRadius = useSharedValue(24);
const cardWidth = useSharedValue(screenWidth - 40);
const cardHeight = useSharedValue(400);
const handleExpand = () => {
cornerRadius.value = withTiming(47, { duration: 600 });
cardWidth.value = withTiming(screenWidth, { duration: 600 });
cardHeight.value = withTiming(screenHeight, { duration: 600 });
};
return (
<TouchableOpacity onPress={handleExpand}>
<ContinuousCornerImageRect
source="https://example.com/photo.jpg"
width={cardWidth}
height={cardHeight}
cornerRadius={cornerRadius}
cornerSmoothing={1.0}
/>
</TouchableOpacity>
);
}📷 Square Image Component (Backward Compatible)
import { ContinuousCornerImage } from 'react-native-continuous-corners';
function App() {
return (
<ContinuousCornerImage
source="https://example.com/image.jpg"
size={200}
cornerRadius={40}
cornerSmoothing={0.6}
/>
);
}Animated Corner Radius
import { ContinuousCornerImageAnimated } from 'react-native-continuous-corners';
import { useSharedValue, withSpring } from 'react-native-reanimated';
import { TouchableOpacity } from 'react-native';
function AnimatedExample() {
const cornerRadius = useSharedValue(20);
const handlePress = () => {
cornerRadius.value = withSpring(60);
};
return (
<TouchableOpacity onPress={handlePress}>
<ContinuousCornerImageAnimated
source="https://example.com/image.jpg"
size={200}
cornerRadius={cornerRadius}
cornerSmoothing={0.6}
/>
</TouchableOpacity>
);
}Container Usage (Cards with Photos + Text) 🆕
import { ContinuousCornerContainer } from 'react-native-continuous-corners';
function ActivityCard() {
return (
<ContinuousCornerContainer
width={350}
height={500}
cornerRadius={24}
cornerSmoothing={0.6}
>
{/* Photo */}
<Image source={{ uri: 'https://example.com/photo.jpg' }} style={{ width: 350, height: 300 }} />
{/* Text */}
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Weekend Hiking</Text>
<Text style={{ fontSize: 16, color: '#666' }}>Join us for an adventure!</Text>
</View>
</ContinuousCornerContainer>
);
}📖 API Reference
ContinuousCornerImage (Static)
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| source | string \| number | Required | Image URL or local resource (require('./image.png')) |
| size | number | Required | Container size (width and height) |
| cornerRadius | number | Required | Corner radius in pixels |
| cornerSmoothing | number | 0.6 | Smoothing factor: 0.0 = standard circle, 0.6 = iOS continuous, 1.0 = maximum smoothing |
| style | ViewStyle | undefined | Additional container styles |
| fit | 'cover' \| 'contain' \| 'fill' \| ... | 'cover' | Image fitting mode (Skia ImageShader fit modes) |
SkiaContinuousCornerImageAnimated (Animated)
Same as above, but cornerRadius accepts a Reanimated SharedValue<number>:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| cornerRadius | SharedValue<number> | Required | Animated corner radius using Reanimated SharedValue |
All other props are identical to the static version.
ContinuousCornerContainer (Container - Static) 🆕
For entire containers with continuous corners (including photos, text, buttons, etc.):
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | Required | Child components (photos, text, buttons, etc.) |
| width | number | Required | Container width |
| height | number | Required | Container height |
| cornerRadius | number | Required | Corner radius in pixels |
| cornerSmoothing | number | 0.6 | Smoothing factor: 0.0 = standard circle, 0.6 = iOS continuous, 1.0 = maximum smoothing |
| style | ViewStyle | undefined | Additional container styles |
| backgroundColor | string | 'transparent' | Container background color |
Platform differences:
- iOS: Uses native
borderCurve='continuous', zero performance overhead ✅ - Android: Uses Skia Canvas clipping, solves Android's lack of continuous corner support 🤖
ContinuousCornerContainerAnimated (Container - Animated) 🆕
Animated version of the container, supports Reanimated animations for cornerRadius (recommended for iOS only).
🎨 Usage Examples
1. User Avatar with Continuous Corner
<SkiaContinuousCornerImage
source={{ uri: user.avatar }}
size={80}
cornerRadius={20}
cornerSmoothing={0.6}
style={{ marginRight: 12 }}
/>2. Photo Gallery with Smooth Corners
const photos = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'];
<View style={{ flexDirection: 'row' }}>
{photos.map((photo, index) => (
<SkiaContinuousCornerImage
key={index}
source={{ uri: photo }}
size={120}
cornerRadius={30}
cornerSmoothing={0.6}
style={{ marginRight: 8 }}
/>
))}
</View>3. Interactive Card with Animated Corners
function InteractiveCard() {
const radius = useSharedValue(20);
const [isExpanded, setIsExpanded] = useState(false);
const handleToggle = () => {
radius.value = withTiming(isExpanded ? 20 : 60, {
duration: 800,
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // iOS-style easing
});
setIsExpanded(!isExpanded);
};
return (
<TouchableOpacity onPress={handleToggle}>
<SkiaContinuousCornerImageAnimated
source="https://example.com/card-image.jpg"
size={300}
cornerRadius={radius}
cornerSmoothing={0.6}
/>
</TouchableOpacity>
);
}4. Different Smoothing Values Comparison
const smoothingValues = [0.0, 0.3, 0.6, 1.0];
<View style={{ flexDirection: 'row' }}>
{smoothingValues.map((smoothing) => (
<View key={smoothing}>
<SkiaContinuousCornerImage
source="https://example.com/image.jpg"
size={100}
cornerRadius={25}
cornerSmoothing={smoothing}
/>
<Text>Smoothing: {smoothing}</Text>
</View>
))}
</View>5. Container with Card Content (Photos + Text + Buttons) 🆕
For complex scenarios like Explore cards (with photos, text, buttons, etc.):
import { ContinuousCornerContainer } from 'react-native-continuous-corners';
// Tinder-style activity card
function ActivityCard({ activity }) {
return (
<ContinuousCornerContainer
width={screenWidth * 0.96}
height={600}
cornerRadius={24}
cornerSmoothing={0.6}
backgroundColor="#FFFFFF"
style={{ margin: 8 }}
>
{/* 📷 Photo */}
<Image
source={{ uri: activity.coverImage }}
style={{ width: '100%', height: 400 }}
/>
{/* 📝 Activity Info */}
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>
{activity.title}
</Text>
<Text style={{ fontSize: 16, color: '#666', marginTop: 8 }}>
{activity.subtitle}
</Text>
{/* 🏷️ Tags */}
<View style={{ flexDirection: 'row', marginTop: 12 }}>
<View style={styles.tag}>
<Text>Hiking</Text>
</View>
<View style={styles.tag}>
<Text>Outdoor</Text>
</View>
</View>
{/* 🔘 Action Button */}
<TouchableOpacity style={styles.button}>
<Text>Join Activity</Text>
</TouchableOpacity>
</View>
</ContinuousCornerContainer>
);
}Why do you need the container version?
- ✅ Android native Image doesn't support continuous corners: Even with
borderCurve='continuous', Android images fall back to standard circular corners - ✅ Cards contain multiple elements: Photos + text + buttons need continuous corners as a whole
- ✅ iOS zero overhead: iOS automatically uses native
borderCurve='continuous' - ✅ Android perfect support: Uses Skia Canvas clipping for completely consistent visual effects
🔬 Technical Details
Algorithm Explanation
This component uses pure cubic Bézier curves to approximate iOS continuous corner radius:
// Control point distance calculation
const controlPointRatio = 0.55228 + smoothness * 0.15;
const controlDistance = radius * controlPointRatio;
// Each corner is drawn with one cubic Bézier curve
path.cubicTo(cp1x, cp1y, cp2x, cp2y, endX, endY);Why This Works:
- Standard Circle Approximation:
0.55228is the magic number for approximating a circle with Bézier curves (4/3 * tan(π/8)) - iOS Continuous Enhancement: Adding
smoothness * 0.15extends the control points further, creating the characteristic "squircle" shape - No Arc Calculations: Unlike
react-native-fast-squirclewhich uses arcs + curves, this uses pure curves, avoiding complex trigonometry
Performance Characteristics
- Rendering: GPU-accelerated via Skia Canvas
- Animation: Runs on UI thread via Reanimated worklets
- Memory: Minimal overhead, path is recomputed only when props change
- Frame Rate: Maintains 60fps+ on modern devices
Comparison with Other Solutions
| Aspect | This Component | borderCurve: 'continuous' | react-native-fast-squircle |
|--------|---------------|----------------------------|------------------------------|
| Android Support | ✅ Perfect | ❌ Falls back to circular | ❌ Cannot clip images |
| Animation | ✅ Smooth 60fps | ✅ Smooth (containers only) | ⚠️ Performance issues |
| Images | ✅ Full support | ✅ Full support | ❌ Android limitation |
| Customization | ✅ Adjustable smoothing | ❌ Fixed | ✅ Adjustable |
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development Setup
- Clone the repository
- Install dependencies:
npm install - Run the example app:
npm start
Guidelines
- Follow the existing code style
- Add tests for new features
- Update documentation as needed
- Ensure TypeScript types are correct
📄 License
MIT License © 2025 Yu Jiawei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.🙏 Acknowledgments
- Inspired by react-native-fast-squircle by Fabrizio Beccaceci
- Algorithm reference: Figma's squircle implementation
- Built with @shopify/react-native-skia
- Animation powered by react-native-reanimated
📞 Contact
- GitHub: @yjw768
- Email: [email protected]
If this project helped you, please consider giving it a ⭐ on GitHub!
