Adding it to an existing app
The Getting started template is the shorter route, verified end to end before each release. For manual setup in a blank or existing Expo app, complete every step below. Each step includes troubleshooting notes.
Angular CLI and Nx generators perform this setup and integrate workspace commands: use
ng add @ng-native/schematics (Angular CLI) or nx g @ng-native/nx:app
(Nx).
Start the project
Create a blank project:
npx create-expo-app my-app --template blank-typescript@sdk-58
cd my-app
rm App.tsx index.ts
npm pkg set main=src/main.tsThe packages support Expo SDK 57 and 58. @sdk-58 starts the app on SDK 58, the SDK the template
is on; without it, create-expo-app takes the newest SDK.
For an existing app: it has to be on SDK 57 or 58. An app on an older SDK moves with
npx expo install expo@^58.0.0, then npx expo install --fix. Remove its entry point, App.tsx
and index.ts or index.js, and run npm pkg set main=src/main.ts, so Expo starts from the
src/main.ts below. Keep the dependencies, native configuration and assets.
For either route, install the framework, configure Metro and write the two app files below.
Install the framework packages
npm install @angular/core @angular/common \
@ng-native/platform @ng-native/fabric \
@ng-native/components @ng-native/device @ng-native/metro<safe-area-view> and <safe-area-provider> use react-native-safe-area-context native views.
Install it if you use them, as most apps do to clear the notch:
npx expo install react-native-safe-area-contextExpo Go bundles it, hiding a missing installation until a development or release build renders
Unimplemented component: <RNCSafeAreaView>.
Add @ng-native/router, @ng-native/expo and @ng-native/icons as needed. Expo modules are
optional peers, so @ng-native/expo installs no native code. Install individual modules such as
expo-haptics for the capabilities you need.
Configure Metro
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withAngularNative } = require('@ng-native/metro/config.cjs');
module.exports = withAngularNative(getDefaultConfig(__dirname));withAngularNative registers Angular's ahead-of-time transformer, compiles component CSS into
engine sheets, and adds source extensions to invalidate external templates. It installs ngDevMode
and animate.enter/animate.leave polyfills before @angular/core loads. Pass { workspaceRoot }
in a monorepo where framework packages sit outside the app's node_modules.
Add the package's config plugin to app.json as well:
{
"expo": {
"plugins": ["@ng-native/metro"]
}
}An app built with the iOS 27 SDK has to adopt the UIKit scene life cycle or it exits at launch, and
the AppDelegate.swift that expo prebuild writes does not. The plugin starts React Native from a
scene delegate instead, passing on the links the app is opened with, and answers the status bar
from view controllers, which is the only way that SDK still lets an app change it.
On Android the plugin changes nothing unless asked. React Native ships layout animations off
there, so LayoutAnimation makes its change without animating
it; passing ["@ng-native/metro", { "android": { "layoutAnimations": true } }] turns them on in
the MainApplication.kt that expo prebuild writes.
Configure TypeScript
// tsconfig.json
{
"extends": "expo/tsconfig.base.json",
"compilerOptions": {
"strict": true,
"allowImportingTsExtensions": true
}
}allowImportingTsExtensions is required. These packages ship TypeScript with .ts imports so
Metro's Angular compiler processes library and app components together. Without this setting,
tsc reports errors for files in node_modules.
extends names Expo's file with its .json. From SDK 58 expo has an exports map, and Vitest
cannot find expo/tsconfig.base without the extension: every test file fails to load with
Failed to load tsconfig 'expo/tsconfig.base'. tsc accepts either name.
Checking templates
tsc checks TypeScript only: a template that binds an input a component does not have, such as
<switch [value]="on"> where the input is checked, compiles, bundles and does nothing. Angular's
own compiler checks templates. With @angular/compiler-cli installed as a development dependency,
add its options to the same tsconfig.json and type-check with ngc instead of tsc:
{
"angularCompilerOptions": {
"strictTemplates": true,
"typeCheckHostBindings": false,
"strictDomEventTypes": false
}
}ngc -p tsconfig.json --noEmittypeCheckHostBindings is off because the components' own host bindings name native props that
Angular's DOM schema does not know. strictDomEventTypes is off because a native event such as
(scroll) is an element event, not an output, so under the DOM schema its $event would be typed
as a DOM Event. A view registered by name, such as registerExpoView('expo-image', 'ExpoImage'),
is used through a component whose inputs are its props, so the template is checked against them:
@ng-native/expo has ExpoImage and the Ui* SwiftUI components, and a view with none yet gets a
small one of the same shape. Leave CUSTOM_ELEMENTS_SCHEMA and NO_ERRORS_SCHEMA out: either turns
template checking off for the whole component.
Write the entry point
// src/main.ts
// Expo's runtime: its fetch, whose response streams a body, and URL, TextDecoderStream and
// structuredClone. Metro runs it before this file only when something imports it, and nothing
// else does in a release build, which would then get React Native's fetch, with no body.
import 'expo';
import { AppRegistry, Image, Platform, processColor } from 'react-native';
import { mount } from '@ng-native/platform';
import { currentConditions, deviceTokens, watchConditions } from '@ng-native/device';
import { getFabricUIManager, registerPlatformComponents } from '@ng-native/fabric';
import { App } from './app/app.ts';
registerPlatformComponents(Platform.OS);
AppRegistry.registerRunnable('main', ({ rootTag }) => {
const app = mount(Number(rootTag), App, getFabricUIManager(), {
processColor,
conditions: currentConditions(),
tokens: deviceTokens(),
resolveAssetSource: (value) => Image.resolveAssetSource(value as never),
});
const stopWatching = watchConditions(app.engine);
app.applicationRef.onDestroy(stopWatching);
});The entry point configures colors, assets and device-dependent styles before rendering:
conditionssupplies@mediavalues.watchConditionsupdates them on rotation or theme changes, makingdark:follow the system. It returns a function that stops it, called as the app is destroyed: Android destroys the app when it recreates the activity, after mounting a new one.tokenssupplies device values such as hairline width. Without it,1pxproduces a thick divider instead of a third of a point on a 3x screen.resolveAssetSourceconvertsrequire('./x.png')to a native-loadable asset; without it, images stay blank.processColorconverts colors to platform integers.
Write the root component
// src/app/app.ts
import { Component, signal } from '@angular/core';
import { Pressable, Text, View } from '@ng-native/components';
@Component({
imports: [Pressable, Text, View],
selector: 'app-root',
template: `
<view class="box">
<text>tapped {{ count() }} times</text>
<pressable (touchEnd)="bump()"><text>tap me</text></pressable>
</view>
`,
styles: `
.box {
flex: 1;
justify-content: center;
padding: 24px;
}
`,
})
export class App {
protected readonly count = signal(0);
protected bump(): void {
this.count.update((n) => n + 1);
}
}Import each element's component, for example imports: [View, Text]. Missing imports render
plain views and trigger development warnings.
Use lowercase element names. An uppercase name such as <View> matches no component's selector;
the Metro transform fails the build on it.
Run npx expo start, then scan the QR code with Expo Go or press i or a for a simulator.
Check that each press increments the counter.
Add Tailwind
Skip Tailwind if you use plain [style] objects.
npm install @ng-native/tailwind tailwindcss @tailwindcss/cli/* src/styles.css */
@import 'tailwindcss/theme.css';
@import 'tailwindcss/utilities.css';
@import '@ng-native/tailwind/native.css';// metro.config.js
const { withTailwind } = require('@ng-native/tailwind/config.cjs');
module.exports = withTailwind(withAngularNative(getDefaultConfig(__dirname)), {
input: './src/styles.css',
});// src/main.ts
import tailwind from '../.angular-native/app.tailwind.js';
// ...and in the mount options, beside processColor:
globalStyles: tailwind,Expo's transform worker returns empty native modules for .css files, so the generated sheet
uses .js. withTailwind runs and watches the Tailwind CLI, updating the sheet when template
classes change without restarting. Metro generates the sheet and its .d.ts in .angular-native/
on startup. Start Metro before the first typecheck, and add .angular-native/ to .gitignore
since it is rebuilt on every start.
See Theming and Tailwind for how classes work without a browser.
The dev loop
Template and style edits hot-swap on the device, preserving component state, and an edited method or function is patched onto the running app. An edit to a field, a constructor, a constant or a selector reloads the app and returns to the page it left; the console logs why.
Where to go next
Continue with Build a form, Working offline, then Deployment.