Docs

Dev tools

Expo SDKExpo CLIExpo MCPExpo GoSnackOrbit

Services

WorkflowsBuildSubmitUpdateHostingLaunchObserve
new
Simulators
preview

Explore

ChangelogExpo Services (EAS)ContactAI
EnterprisePricingBlog
50K
Log in
Sign up

Site footer

Expo

Newsletter

Stay in touch with all things Expo

Product

  • Star us on GitHub
  • Expo CLI on GitHub
  • Expo Services (EAS)
  • EAS CLI on GitHub
  • Expo Go
  • Expo Orbit
  • Snack

Resources

  • Documentation
  • Blog
  • Changelog
  • Support
  • Trust Center
  • Join Discord

Solutions

  • Enterprise
  • Startup
  • Solo devs
  • React web devs
  • E-commerce
  • Crypto
  • Finserv
  • QSR

Company

  • Home
  • Pricing
  • Customers
  • Consultants
  • About
  • Branding
  • Careers

Legal

  • Terms of service
  • Acceptable use policy
  • Privacy policy
  • Privacy explained
  • Cookie policy
  • Security & Compliance
  • Enterprise trust
  • Community guidelines
© 2026 650 Industries, Inc.
All systems operational

January 8, 2026::Development·Users

Cutout camera effects in React Native with Expo Camera and MaskedView

Learn how to use Expo Camera to create a stencil-like cutout effect that turns a live feed into a frosted glass background.

Mehdi Davoodi

Mehdi Davoodi

Guest Author

How to build cutout camera effects in React Native with Expo Camera and MaskedView

Contents

  • What we're building
  • Setting up Expo Camera
  • Understanding camera permissions
  • Combining with MaskedView
  • How it works:
  • Using it with the camera
  • Adding the blur effect

This is a guest post from Mehdi Davoodi - he builds React Native and Expo UI experiments and shares what I learns on 𝕏: @mehdi_made

...


I wanted to build something where the camera feed shows through text and UI elements, like a cutout effect. The idea was simple: use the live camera as a dynamic background, but only reveal it through specific parts of the UI.

Here's how I did it with Expo Camera, MaskedView, and Expo Blur.

What we're building

Essentially it's screen that shows a user list. But instead of a solid background, the camera feed shows through the text and avatars. On top of that, there's a blur layer that gives it a frosted glass look. The overall effect is a UI that feels alive, unique, and memorable.

The key pieces:

  • expo-camera for the live camera feed
  • @react-native-masked-view/masked-view to create the cutout effect
  • expo-blur for the frosted glass layer

Setting up Expo Camera

First, install the package:

The expo-camera package gives you two main things: a CameraView component and a useCameraPermissions hook. The hook is the important part; you cannot just render the camera without asking for permission first.

Here's the basic setup:

That’s the minimum. But there’s a catch: the permission object has more states than just granted or not.

Understanding camera permissions

The permission object from useCameraPermissions has two important properties:

  • granted - whether you have permission
  • canAskAgain - whether you can prompt the user again

This gives you four actual states:

  1. permission is null → still checking
  2. permission.granted is true → camera is ready
  3. permission.granted is false and canAskAgain is true → user denied, but you can ask again
  4. permission.granted is false and canAskAgain is false → user blocked it in settings

That last one is important. If someone denies permission and checks "Don't ask again", you can't show the prompt anymore. You need to tell them to go to settings manually.

I wrapped this logic in a hook:

Now my component just checks isReady and doesn't worry about the details.

Combining with MaskedView

MaskedView is a component that lets you use another component as a mask. Think of it like a stencil—you cut shapes out, and whatever's behind shows through those shapes.

How it works:

MaskedView has two parts:

  1. maskElement - the stencil (what defines the shape)
  2. children - what shows through the stencil

The mask works based on opacity. Anything fully opaque (like black) in the mask reveals the children. Anything transparent hides them. Gray values give you partial transparency.

Using it with the camera

Here's where it gets interesting. Instead of a solid color behind the mask, we use the live camera feed:

The camera only shows through the text. Everything else is transparent.

Adding the blur effect

I wanted a frosted glass look, not a raw camera feed. So I layered BlurView from expo-blur on top of the camera:

The blur sits on top of the camera (using zIndex or render order), and the mask cuts through both. So you see blurred camera through the mask shapes.

Here is the repo. I hope it's useful. If you do end up building something based on what you've learned in this blog I would love if you could share it with me.

Also, if there are other UI experiments you'd like to see me build and share please let me know in a DM. Thanks for reading!

Share article

Related articles

All posts →
How to create Apple maps style liquid glass sheets in Expo

November 25, 2025

How to create Apple Maps style liquid glass sheets in Expo (the real way)

Integrating iOS 26 Liquid Glass app with Expo UI and SwiftUI

September 12, 2025

Integrating iOS 26 Liquid Glass app with Expo UI and SwiftUI

npx expo install expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera';
const App = () => {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) {
// Still loading
return <View />;
}
if (!permission.granted) {
// Need to ask
return (
<Button title="Grant Access" onPress={requestPermission} />
);
}
return <CameraView style={{ flex: 1 }} />;
};
import { useEffect, useCallback } from 'react';
import { useCameraPermissions } from 'expo-camera';
export const useCamera = (autoRequest = true) => {
const [permission, requestCameraPermission] = useCameraPermissions();
const getStatus = () => {
if (!permission) return 'loading';
if (permission.granted) return 'granted';
if (!permission.canAskAgain) return 'blocked';
return 'denied';
};
const status = getStatus();
const requestPermission = useCallback(async () => {
try {
await requestCameraPermission();
} catch (error) {
console.error('Failed to request camera permission:', error);
}
}, [requestCameraPermission]);
// Auto-request on mount
useEffect(() => {
if (autoRequest && status === 'denied') {
requestPermission();
}
}, [autoRequest, status, requestPermission]);
return {
status,
isReady: status === 'granted',
requestPermission,
};
};
import MaskedView from '@react-native-masked-view/masked-view';
<MaskedView
maskElement={
<View style={{ backgroundColor: 'transparent', flex: 1 }}>
<Text style={{ color: 'black', fontSize: 60 }}>Hello</Text>
</View>
}
>
{/* This is what shows through */}
<View style={{ flex: 1, backgroundColor: 'blue' }} />
</MaskedView>
<MaskedView
maskElement={
<Text style={{ color: 'black', fontSize: 40 }}>Hello</Text>
}
>
<CameraView style={{ flex: 1 }} />
</MaskedView>
<MaskedView
style={{ flex: 1 }}
maskElement={<MaskContent />}
>
<BlurView style={StyleSheet.absoluteFillObject} intensity={100} />
<CameraView style={StyleSheet.absoluteFillObject} />
</MaskedView>