Extending with Jetpack Compose
Edit page
Learn how to create custom Jetpack Compose components and modifiers that integrate with Expo UI.
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
This guide explains how to create custom Jetpack Compose components and modifiers that integrate seamlessly with Expo UI.
Prerequisites
Before you begin, make sure you have:
@expo/uiinstalled in your project. See Building Jetpack Compose apps with Expo UI for more information.- A development build of your app (Expo UI is not available in Expo Go).
- Basic familiarity with Expo Modules API and Jetpack Compose.
-Â npx expo install @expo/uiCreating a custom component
Project setup
1
Create a local Expo module in your project:
-Â npx create-expo-module@latest --local my-ui2
Update your module's android/build.gradle to enable Jetpack Compose and depend on expo-ui. The lines marked below are added on top of the default scaffold:
// Pull in the Kotlin Compose compiler plugin classpath. buildscript { repositories { mavenCentral() } dependencies { classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${kotlinVersion}") } } apply plugin: 'com.android.library' apply plugin: 'expo-module-gradle-plugin' apply plugin: 'org.jetbrains.kotlin.plugin.compose' // Apply the Compose compiler plugin. // ... group / version android { // ... namespace, defaultConfig // Turn on Jetpack Compose for this module. buildFeatures { compose true } } // Depend on `expo-ui` plus the Compose libraries you use. dependencies { if (findProject(':expo-ui') != null) { implementation project(':expo-ui') } else { implementation 'expo.modules.ui:expo.modules.ui:+' } implementation 'androidx.compose.foundation:foundation-android:1.10.6' implementation 'androidx.compose.ui:ui-android:1.10.6' implementation 'androidx.compose.material3:material3:1.5.0-alpha17' }
Creating a Compose view
3
Create your Compose view. It has two parts:
- Props data class: annotated with
@OptimizedComposeProps, implementsComposeProps, and includes amodifiers: ModifierListfield for themodifiersprop. @Composablecontent function: an extension onFunctionalComposableScopeso it can callModifierRegistry.applyModifiers(...)and renderChildren(...).
package expo.modules.myui import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import expo.modules.kotlin.views.ComposeProps import expo.modules.kotlin.views.FunctionalComposableScope import expo.modules.kotlin.views.OptimizedComposeProps import expo.modules.ui.ModifierList import expo.modules.ui.ModifierRegistry import expo.modules.ui.UIComposableScope @OptimizedComposeProps data class MyCustomViewProps( val title: String = "", val modifiers: ModifierList = emptyList() ) : ComposeProps @Composable fun FunctionalComposableScope.MyCustomViewContent(props: MyCustomViewProps) { Column( modifier = ModifierRegistry.applyModifiers( props.modifiers, appContext, composableScope, globalEventDispatcher ) ) { Text(text = props.title, style = MaterialTheme.typography.titleMedium) Children(UIComposableScope()) // Renders React children } }
4
Register the view in your module using ExpoUIView. This wires your @Composable content into the Expo modules view system and makes it available to JavaScript:
package expo.modules.myui import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.ui.ExpoUIView class MyUiModule : Module() { override fun definition() = ModuleDefinition { Name("MyUi") ExpoUIView<MyCustomViewProps>("MyCustomView") { Content { props -> MyCustomViewContent(props) } } } }
5
Create a wrapper component that connects modifiers with event handling. The createViewModifierEventListener utility enables event-based modifiers like clickable and onVisibilityChanged to work with your custom view:
import { type PrimitiveBaseProps } from '@expo/ui/jetpack-compose'; import { createViewModifierEventListener } from '@expo/ui/jetpack-compose/modifiers'; import { requireNativeView } from 'expo'; export interface MyCustomViewProps extends PrimitiveBaseProps { title: string; children?: React.ReactNode; } const NativeMyCustomView = requireNativeView<MyCustomViewProps>('MyUi', 'MyCustomView'); export function MyCustomView({ modifiers, ...restProps }: MyCustomViewProps) { return ( <NativeMyCustomView modifiers={modifiers} {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)} {...restProps} /> ); }
Using your custom component
Your custom component now works with all @expo/ui built-in modifiers:
import { Host, Text } from '@expo/ui/jetpack-compose'; import { background, clip, paddingAll } from '@expo/ui/jetpack-compose/modifiers'; import { MyCustomView } from './modules/my-ui'; export default function App() { return ( <Host style={{ flex: 1 }}> <MyCustomView title="Hello World" modifiers={[ paddingAll(16), background('#f0f0f0'), clip({ type: 'roundedCorner', radius: 12 }), ]}> <Text>Child content</Text> </MyCustomView> </Host> ); }
Creating custom modifiers
You can also create custom modifiers that work with any Expo UI component.
Modifiers are Compose's way to configure layouts for styling, sizing, behavior, and more. Learn more in Android's Compose modifiers documentation.
Native modifier implementation
1
Define your modifier's parameters as an @OptimizedRecord data class, and a function that returns a Modifier from those params:
package expo.modules.myui import android.graphics.Color import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record import expo.modules.kotlin.types.OptimizedRecord import expo.modules.ui.compose @OptimizedRecord data class CustomBorderParams( @Field val color: Color? = null, @Field val width: Int = 2, @Field val cornerRadius: Int = 0 ) : Record fun customBorderModifier(params: CustomBorderParams): Modifier { return Modifier.border( border = BorderStroke(params.width.dp, params.color.compose), shape = RoundedCornerShape(params.cornerRadius.dp) ) }
compose is a Kotlin extension property on android.graphics.Color? defined in the expo.modules.ui package. Importing it with import expo.modules.ui.compose lets you call params.color.compose to convert the Android Color parsed from JS into the androidx.compose.ui.graphics.Color that Compose APIs (like BorderStroke) expect. It's the same helper Expo UI's built-in modifiers use.
2
Register your modifier with ModifierRegistry in your module definition. Use OnCreate to register and OnDestroy to unregister so the factory does not leak across module reloads:
package expo.modules.myui import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.records.recordFromMap import expo.modules.ui.ExpoUIView import expo.modules.ui.ModifierRegistry class MyUiModule : Module() { override fun definition() = ModuleDefinition { Name("MyUi") OnCreate { ModifierRegistry.register("customBorder") { map, _, _, _ -> customBorderModifier(recordFromMap<CustomBorderParams>(map)) } } OnDestroy { ModifierRegistry.unregister("customBorder") } ExpoUIView<MyCustomViewProps>("MyCustomView") { Content { props -> MyCustomViewContent(props) } } } }
The register lambda receives the raw map sent from JavaScript, the current ComposableScope (use it for scope-dependent modifiers like weight or align), the AppContext, and an event dispatcher. Most modifiers only need map and convert it via recordFromMap<T>(map).
JavaScript modifier function
3
Create a TypeScript function that builds the modifier config:
import { createModifier } from '@expo/ui/jetpack-compose/modifiers'; import { type ColorValue } from 'react-native'; export const customBorder = (params: { color?: ColorValue; width?: number; cornerRadius?: number; }) => createModifier('customBorder', params);
4
Export the modifier from your module:
export { MyCustomView, type MyCustomViewProps } from './src/MyCustomView'; export { customBorder } from './src/modifiers';
Using custom modifiers
Your custom modifier works with any @expo/ui component:
import { Column, Host, Text } from '@expo/ui/jetpack-compose'; import { paddingAll } from '@expo/ui/jetpack-compose/modifiers'; import { customBorder } from './modules/my-ui'; export default function App() { return ( <Host style={{ flex: 1 }}> <Column modifiers={[paddingAll(20), customBorder({ color: '#FF6B35', width: 3, cornerRadius: 8 })]}> <Text>This has a custom border!</Text> </Column> </Host> ); }
Next steps
Congratulations! You've learned how to extend Expo UI with custom Jetpack Compose components and modifiers. Your custom components now integrate seamlessly with the built-in modifier system.
Here are some ideas for what to build next:
- Use the built-in Jetpack Compose components that come with Expo UI.
- Build custom modifiers for app-specific styling patterns.
- Wrap third-party Compose libraries for use in React Native.
- Share your components as an npm package for others to use.