---
title: "Bringing CSS Clipping to React Native "
description: "A deep look at how a single CSS feature moves through React Native’s New Architecture, from JavaScript styles to native rendering on iOS and Android."
url: https://www.callstack.com/blog/bringing-css-clipping-to-react-native
updated: 2026-10-24T08:39:28.041Z
---

# Bringing CSS Clipping to React Native 

A deep look at how a single CSS feature moves through React Native’s New Architecture, from JavaScript styles to native rendering on iOS and Android.

_By Kamil Paradowski, Software Engineer @ Callstack · Published 2026-01-28 · Category: React Native_

If you've ever tried to create angled headers or any other non-rectangular section dividers in React Native, you've probably felt that something is missing. On the web, CSS `clip-path` makes these shapes trivial.`‍`‍

```css
.star-badge {
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
```

But sadly, it’s not available for React Native apps, so we need to import a third-party library:`‍`‍

```typescript
import {MaskedView} from ...

<MaskedView path="..." />
```

The effect of this code is this pretty shape:

![](https://www.callstack.com/assets/6979f8186ba19d89615f7e58_Bringing-CSS-Clipping-to-React-Native_-The-effect-of-the-code_.webp)

We ended up with cross-platform star-shape masking. The code, however, looks quite different for web and mobile platforms. In this article I’d like to explore what it would take for React Native to support masking with `clip-path` natively through the CSS-like StyleSheet API and propose solutions.

## What is CSS `clip-path`?

The `clip-path` CSS property defines a clipping region that acts like a mask: only the parts of an element that fall inside the region are visible, while everything outside is hidden. This region is described by two components: the shape itself and the geometry box that determines how that shape is positioned and sized.

### The Shape Function

This defines the geometry of your cut. You can use helper shape functions (`circle()`, `polygon()`, `inset()`, …) or point to a local or remote resource (`url(image.png)`).

> SVG images won’t be supported in the first version of `clip-path` implementation for React Native; `path()` shape does not work.

### The Geometry Box

This is the reference frame. It tells the renderer which part of the element's "box" to use as the starting point for your shape's coordinates `(0,0)`. The default is `border-box`, but you can also use `padding-box`, `content-box` or `margin-box`.

![__wf_reserved_decorative](https://www.callstack.com/assets/6978b53f7627ec3f9b04102e_margin.jpg)

_The Geometry Box - Margin-box reference box_

## `clip-path` in Action

```css
.avatar {
  clip-path: circle(50%);
}

.star-badge {
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
```

Styles as presented above should result in the following view.

![](https://www.callstack.com/assets/6978b587ffb16fe4e5478dd9_image-(27).png)

_Clip-Path in Action - View Result_

## Why React Native Is Different

On the web, the rendering stack is unified under a single engine such as Blink. It parses CSS and computes layout. Then it uses a graphics engine like Skia to paint every pixel on the screen.

In contrast, React Native doesn't draw its own UI by default. Instead, React Native acts as a coordinator. It translates your styles into layout instructions, which the host operating system (iOS or Android) draws using native UI kits.

Essentially, the web simulates a UI inside a browser window, while React Native assembles a UI using the phone's native building blocks.

At the same time, React Native gives us a subset of CSS to use, but not everything. Let's see how this works in practice using the `clip-path` example.

## From Style to Screen

React Native architecture can be treated as a distributed system. It coordinates work across various pieces. This means that to add support for the new attribute, we have to touch and modify a lot of different parts of it.

Let's tackle them one by one.

The journey starts when you write a sample `clipPath: 'circle(50% at 25% 25%)'` definition in your styles.

### Starting at the JS Layer

React files have to be aware of the appearance of our new attribute, thus we need to ensure its visibility to TypeScript or Flow. Its argument is just the text, so it should be as easy as the following change, right?

```diff
export type ____ViewStyle_InternalBase = $ReadOnly<{
   // ...
   isolation?: 'auto' | 'isolate',
+  clipPath?: string,
}>;
```

‍`‍`Well, not really. The JS layer is also responsible for parsing the argument's value and converting it into an object structure, that is later processed by the native code. It means that we also need to add parsing functionality and relevant types that describe the possible new `clipPath` input schema.

It has to come along with tests and a few other minor touches. I will skip these parts to keep this article short and to-the-point. Let's see what happens when our new CSS attribute value, in form either of parsed object or a raw string, is passed on to the React Native C++ core layer.

> Recently React Native core team added a new, [native CSS parser](https://github.com/facebook/react-native/tree/main/packages/react-native/ReactCommon/react/renderer/css). It means that attribute text values can be parsed, tokenized and converted to native types directly by C++ code! This not only offers a slight performance boost but is also the technically correct approach. However, it's currently in development and hidden by an internal feature flag. I am looking forward to seeing it enabled by default.

### C++ as the Source of Truth

React Native's new architecture renderer (a.k.a. Fabric) uses C++ as a shared core between platforms. This is the layer that enables true cross-platform compatibility. The received clip path data needs to be represented in C++ so that both iOS and Android (and other platforms such as Windows, which recently [adopted New Architecture by default in v0.81](https://devblogs.microsoft.com/react-native/%F0%9F%9A%80react-native-windows-v0-81-is-here/)) can consume it.

But let's start from the beginning. To understand how a change of CSS property results in a re-rendered application, we need to walk end-to-end and briefly explain the render pipeline of React Native.

Let's see step by step what changes are required at any specific point to add our new CSS attribute functionality.

![](https://www.callstack.com/assets/6978b9a8752cbf9f2fafb87a_React-%E2%80%93-Event-handler.jpg)

_CSS Attribute Functionality - Steps Required_

> If you would like to dive deeper into React Native's new render pipeline, it is very well described in the [official documentation](https://reactnative.dev/architecture/overview) of the New Architecture, especially check the [Render, Commit, and Mount](https://reactnative.dev/architecture/render-pipeline) section.

Everything starts from the process called **Reconciliation**. During this stage, called **Render Phase**, React's reconciler computes the diff between old and new props, detects changes to props, and schedules a re-render. The changes in this step were discussed in the previous paragraph.

When props change, React clones _Shadow Node_ with new props. Unlike the old architecture, where this was a slow, asynchronous message to a separate thread, this now happens through direct JSI calls.

Why cloning is helpful here?

_Shadow Tree_ _Nodes_ are immutable due to thread safety reasons. To change the CSS attribute, the renderer cannot mutate the existing node. It has to clone the affected _Shadow Node_ with the new prop. It uses structural sharing to refer back to the old nodes that didn't change, minimizing memory overhead.

That’s the place we need to step in.

_Shadow Node_ is a representation of a _React Shadow Tree_ element. It contains props that originate from JavaScript along with layout information. We need to expand the list of properties and add a new one.

```diff
namespace facebook::react {

class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
	 // ...
	 
   // Color
   Float opacity{1.0};
   SharedColor backgroundColor{};

   // Borders
   CascadedBorderRadii borderRadii{};
   CascadedBorderColors borderColors{};
   
 	 // ...
+  std::optional<ClipPath> clipPath{};
}

} // namespace facebook::react
```

_Integrating clipPath into the BaseViewProps class definition_

`std::optional<T>` means in the C++ world that the property is nullable, and `ClipPath` is our new type that represents this attribute. It generally looks like this:

```cpp
namespace facebook::react {

// Define individual geometric primitives used for clipping
struct CircleShape {
  std::optional<ValueUnit> r{};
  std::optional<ValueUnit> cx{};
  std::optional<ValueUnit> cy{};
	// ...
};

struct EllipseShape {
  std::optional<ValueUnit> rx{};
  std::optional<ValueUnit> ry{};
  std::optional<ValueUnit> cx{};
  std::optional<ValueUnit> cy{};
  // ...
}
...
// A type-safe union representing any possible basic shape
using BasicShape = std::variant<
	CircleShape,
	EllipseShape,
	InsetShape,
	PolygonShape,
	RectShape,
	XywhShape>;

// Standard box model enumerations for the clip reference box
enum class GeometryBox : uint8_t {
  MarginBox,
  BorderBox,
  ContentBox,
  PaddingBox
};

// Main structure representing a full clip-path value
struct ClipPath {
  std::optional<BasicShape> shape;
  std::optional<GeometryBox> geometryBox;

  bool operator==(const ClipPath &other) const;

// Debugging and serialization helpers
#if RN_DEBUG_STRING_CONVERTIBLE
  std::string toString() const;
#endif

#ifdef RN_SERIALIZABLE_STATE
  folly::dynamic toDynamic() const;
#endif
};

} // namespace facebook::react
```

_Data structures for the clip-path property implementation_

We need to ensure that, as the CSS specification says, the `clip-path` attribute should result in the creation of a [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Positioned_layout/Stacking_context).

It requires a small modification of `ViewShadowNode` class, which is the _Shadow Node_ representation of `<View>` component to be mounted. Lack of this adjustment would lead to unexpected results, such as not clipping children of the component.

```diff
void ViewShadowNode::initialize() noexcept {
  // ...
  bool formsStackingContext = !viewProps.collapsable ||
      viewProps.pointerEvents == PointerEventsMode::None ||
      !viewProps.nativeId.empty() || viewProps.accessible ||
      viewProps.opacity != 1.0 || viewProps.transform != Transform{} ||
      (viewProps.zIndex.has_value() &&
       viewProps.yogaStyle.positionType() != yoga::PositionType::Static) ||
      viewProps.yogaStyle.display() == yoga::Display::None ||
      viewProps.getClipsContentToBounds() || viewProps.events.bits.any() ||
      isColorMeaningful(viewProps.shadowColor) ||
      viewProps.accessibilityElementsHidden ||
      viewProps.accessibilityViewIsModal ||
      viewProps.importantForAccessibility != ImportantForAccessibility::Auto ||
      viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto ||
      !viewProps.filter.empty() ||
      viewProps.mixBlendMode != BlendMode::Normal ||
      viewProps.isolation == Isolation::Isolate ||
      HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) ||
      !viewProps.accessibilityOrder.empty() ||
+     viewProps.clipPath.has_value();
  // ...
  if (formsStackingContext) {
    traits_.set(ShadowNodeTraits::Trait::FormsStackingContext);
  } else {
    traits_.unset(ShadowNodeTraits::Trait::FormsStackingContext);
  }
  // ...
}
```

`Updating Stacking Context logic to include clip-path`

`‍`When **Rendering** is finished, commit of _Shadow Tree_ is triggered. It promotes the new tree as the “next tree” to be mounted, and also schedules calculation of layout (_Yoga_).

The next, and the last phase is **Mounting**: transforming _React Shadow Tree_ into a _Host View Tree_.

It includes three steps:

- **Tree Diffing**, where computation of the diff between previous and the next tree happens.
- **Tree Promotion** to ensure the next mount phase computes a diff against proper tree.
- **View Mounting**, where the actual rendering changes have to occur.

Okay, we know how information about a property change is propagated from JavaScript up to the Host Platform level. Let's now explore how the actual implementation of `clip-path` looks for iOS and Android.

### Clipping on iOS

React Native uses UIKit to handle and build UI elements, and its most fundamental building block is `UIView`. In UIKit, every `UIView` is backed by a `CALayer` (Core Animation Layer). While the view handles user interactions like touches, the layer is what actually renders the content on the screen and acts like a canvas. Each layer has a mask property that acts as a transparency mask - telling what should be visible and what should be hidden. With that knowledge, let's get down to implementation!

Fortunately for us, there is one custom React Native iOS class (`RCTViewComponentView`) that extends `UIView` and every type of created `View`, no matter if it is `<View>` or `<Image>`, inherits from it. This means we only need to add our `clip-path` logic there and it will work for every React Native component! The place we are looking for is `invalidateLayer` function, it has to be extended by the block of code shown below.

```objectivec
- (void)invalidateLayer
{
	// ...
  // Handle clip-path property
  if (_props->clipPath.has_value()) {
    CALayer *maskLayer = [RCTClipPathUtils createClipPathLayer:_props->clipPath.value()
                                                 layoutMetrics:_layoutMetrics
                                                     yogaStyle:_props->yogaStyle
                                                        bounds:layer.bounds
                                                   cornerRadii:RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii)];
    if (maskLayer != nil) {
      self.currentContainerView.layer.mask = maskLayer;
    }
  }
  // ...
}
```

_Modifications to invalidateLayer function_`‍`

`‍`Where `createClipPathLayer` is a helper function responsible for calculating the `geometry-box` and creating the `shape` path. Let’s take a look at its breakdown.

```objectivec
+ (CALayer *)createClipPathLayer:(const ClipPath &)clipPath
                   layoutMetrics:(const LayoutMetrics &)layoutMetrics
                       yogaStyle:(const facebook::yoga::Style &)yogaStyle
                          bounds:(CGRect)bounds
                     cornerRadii:(RCTCornerRadii)cornerRadii
{
  CGRect box = [self getGeometryBoxRect:clipPath.geometryBox
		                      layoutMetrics:layoutMetrics
			                        yogaStyle:yogaStyle
		                             bounds:bounds];
```

`‍`1. Firstly, we need to determine the correct geometry-box bounds. We need to use the proper reference box and adjust its corner radii.

```objectivec
  UIBezierPath *path = nil;
  if (clipPath.shape.has_value()) {
	  // clipPath: circle(50%) content-box
    path = [RCTBasicShapeUtils createPathFromBasicShape:clipPath.shape.value() bounds:box];
  } else if (clipPath.geometryBox.has_value()) {
	  // clipPath: content-box
    path = [UIBezierPath bezierPathWithCGPath:RCTPathCreateWithRoundedRect(box, cornerInsets, nil, NO)];
  }
```

2. UIKit comes with a helper UIBezierPath class for path operations. For each supported shape, we need to create a path describing it using our new helper createPathFromBasicShape function. If a shape is defined, we create a corresponding path. Otherwise, we use geometry-box path, if one is provided.

```objectivec
  // Skip invalid or empty path
  if (path == nil) {
    return nil;
  }
  
  CAShapeLayer *maskLayer = [CAShapeLayer layer];
  maskLayer.path = path.CGPath;
  return maskLayer;
}
```

3. If outcome `path` is correct, mask layer can be created from it using `CAShapeLayer` class. This mask layer is used as a mask of `UIView` in `invalidateLayer` function.

What does `createPathFromBasicShape` look like? Below you can find an example for the `rect` shape.

```objectivec
+ (UIBezierPath *)createRectPath:(const RectShape &)rect bounds:(CGRect)bounds
{
  // Define corners
  CGFloat top = bounds.origin.y + RCTResolveValueUnit(rect.top, bounds.size.height);
  CGFloat right = bounds.origin.x + RCTResolveValueUnit(rect.right, bounds.size.width);
  CGFloat bottom = bounds.origin.y + RCTResolveValueUnit(rect.bottom, bounds.size.height);
  CGFloat left = bounds.origin.x + RCTResolveValueUnit(rect.left, bounds.size.width);

	// Create a rect
  CGRect clipRect = CGRectMake(left, top, right - left, bottom - top);

	// Catch for invalid dimensions
  if (clipRect.size.width < 0 || clipRect.size.height < 0) {
    return nil;
  }

  // Adjust radius
  CGFloat borderRadius = rect.borderRadius.has_value() ? RCTResolveValueUnit(rect.borderRadius.value(), MIN(clipRect.size.width, clipRect.size.height)) : 0.0f;
  
  // Use bezierPathWithRoundedRect to create rounder rect bezier path
  return [UIBezierPath bezierPathWithRoundedRect:clipRect cornerRadius:borderRadius];
}
```

_Creation of rect path on iOS_

Combining all together, we got working `clip-path` for iOS in React Native!

Of course, I skipped some implementation details. These include detecting attribute changes, marking layers as requiring invalidation, parsing attribute text values natively, converting CSS types into graphics types, and a few other things. This should still give you the overall gist of how the process works. If you’re curious, you can find links to all the changes [here](https://github.com/facebook/react-native/pulls?q=is%3Apr+is%3Aopen+%22add+clip-path%22).

### Clipping on Android

Android design is a bit different, which is not a surprise.

Every React Native component for Android is represented as a `View` class. While alternatives like `ViewOutlineProvider` exist for simple rounded corners, they are often too limited for complex shapes.

The most popular and suitable solution for our case for clipping of Android's `View` is to override `draw` method and apply clipping directly to the canvas. Android `Canvas` class provides dedicated functions for this functionality, which are `clipPath` and `clipRect` - that's exactly what we need!

What is different for Android from iOS is that there is no base `View` class that is used for all types of components (there are some exceptions which break the general solution). However, there is a special React Native helper class `BackgroundStyleApplicator`, that is used and available for every `View`.

We can add our `clip-path` logic there, and call it from all other places. For `ReactImageView` class, that represents `<Image>` component, code would look like this:

```kotlin
public override fun draw(canvas: Canvas) {
  BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) {
    super.draw(canvas)
  }
}
```

_Overriding draw function for clipping purposes_

Where `applyClipPathIfPresent` is a function that can be presented in simplified version as:

```kotlin
public fun applyClipPathIfPresent(view: View, canvas: Canvas, drawContent: (() -> Unit?)?) {
    // ...
    canvas.withSave {
			// ...
			// Calculation of geometry-box
      val bounds = getGeometryBoxBounds(...)
      
      // Path creation
      val path: Path? = if (clipPath.shape != null) {
    	  // clipPath: circle(50%) content-box
        ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds)
      } else if (clipPath.geometryBox != null) {
        // clipPath: content-box
	      ClipPathUtils.createRoundedRectPath(bounds,
		      GeometryBoxUtil.adjustBorderRadiusForGeometryBox(view, clipPath.geometryBox, ...)
	      )
      } else {
        null
      }

      // Clipping logic
      if (path != null) {
        clipPath(path)
      }

		  // Draw actual content to be clipped
      drawContent?.invoke()
    }
  }
```

_Clipping Android View class_

The logic is very similar to iOS:

1. First, we calculate `geometry-box` using a new helper function `getGeometryBoxBounds`.
2. Then we create a path describing the shape passed to `clip-path`.
3. And call the relevant `Canvas` method to clip the drawing region.

Below you can find how to create a `rect` shape for Android:

```kotlin
 private fun createRectPath(rect: RectShape, bounds: RectF): Path? {
    // Define corners
    val top = bounds.top + resolveLengthPercentage(rect.top, bounds.height())
    val right = bounds.left + resolveLengthPercentage(rect.right, bounds.width())
    val bottom = bounds.top + resolveLengthPercentage(rect.bottom, bounds.height())
    val left = bounds.left + resolveLengthPercentage(rect.left, bounds.width())

		// ...
		// Create path
    val path = Path()
    if (rect.borderRadius != null) {
      // Rounded corners case
      val referenceDimension = minOf(rect.width(), rect.height())
      val radius = resolveLengthPercentage(borderRadius, referenceDimension)
      path.addRoundRect(rect, radius, radius, Path.Direction.CW)
    } else {
      // Sharp edges case
      path.addRect(rect, Path.Direction.CW)
    }

    return path
  }
```

_Creation of rect path on Android_

Everything is wrapped with `withSave` block. It is done this way so that our modifications to canvas do not affect other drawing calls, as it could lead to unexpected behaviors.

What about clipping `View` background, children or its effects? We need to quickly examine the Android graphics pipeline and check what `draw(Canvas)` method actually does. It can be easily checked by looking at `View` class source code:`‍`‍

```kotlin
// Step 1, draw the background, if needed
drawBackground(canvas)
// Step 2, save the canvas' layers
// ...
// Step 3, draw the content
onDraw(canvas)
// Step 4, draw the children
dispatchDraw(canvas)
// Step 5, draw the fade effect and restore layers
// ...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas)
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas)
```

_Android graphics pipeline order_

Applying clipping to the canvas before `super.draw(canvas)` is called ensures that every drawable part of component will be clipped by our code. We just need to add it to all `View` classes, and our job is done! That was easy, right?

As you can guess, I oversimplified the process again. Initially, it also required a bit more work. We needed to perform type conversions, add adjustments to support the new attribute in several other places, and the most interesting one: compute and cache layout information during Mounting process for proper `geometry-box` calculation.

However that could be a topic for a separate article and I hope this description provided you with a base idea.

![](https://www.callstack.com/assets/6979f84fde294c8f19ce5d84_Bringing-CSS-Clipping-to-React-Native_-Final-effect.webp)

## Wrapping Up

Like many other mobile developers, I enjoy building apps in React Native. I can reuse my web skills and apply them to build apps for iOS, Android, and other platforms.

However it’s not without hiccups. Some things that are easy with CSS on the web, require me to use a third-party library that encapsulates native views. A new thing to learn.

By bringing more CSS support to React Native I hope we’ll get closer to the world where we can use all of our web styling knowledge in our mobile apps.

I hope you liked our small exploration of how CSS and the New Architecture work under the hood in React Native, and learned something new.

Three PRs that bring `clip-path` support to React Native are [currently under review](https://github.com/facebook/react-native/pulls?q=is%3Apr+is%3Aopen+%22add+clip-path%22). When they are merged and released, I hope you’ll build something cool with it!
