# Assorted

## Authentication with OAuth or OpenID providers

Learn how to utilize the expo-auth-session library to implement authentication with OAuth or OpenID providers.

[`expo-auth-session`](/versions/latest/sdk/auth-session) provides a unified API for implementing OAuth and OpenID Connect providers on Android, iOS, and web. This guide will show you how to use the `AuthSession` API using a few examples.
## Rules for all authentication providers
When using the `AuthSession` API, the following rules apply to all authentication providers:
- Use `WebBrowser.maybeCompleteAuthSession()` to dismiss the web popup. If you forget to add this then the popup window will not close.
- Create redirects with `AuthSession.makeRedirectUri()` this does a lot of the heavy lifting involved with universal platform support. Behind the scenes, it uses `expo-linking`.
- Build requests using `AuthSession.useAuthRequest()`, the hook allows for async setup which means mobile browsers won't block the authentication.
- Be sure to disable the prompt until `request` is defined.
- You can only invoke `promptAsync` in user interaction on the web.
- Expo Go cannot be used for local development and testing of OAuth or OpenID Connect-enabled apps due to the inability to customize your app scheme. You can instead use a [Development Build](/develop/development-builds/introduction/), which enables an Expo Go-like development experience and supports OAuth redirects back to your app after login in a manner that works just like it would in production.
## Obtaining access tokens
Most providers use the [OAuth 2](https://oauth.net/2/) standard for secure authentication and authorization. In the authorization code grant, the identity provider returns a one-time code. This code is then exchanged for the user's access token.
Since [your client application code is not a secure place to store secrets](https://reactnative.dev/docs/security#storing-sensitive-info), it is necessary to exchange the authorization code in a server such as with [API routes](/router/web/api-routes) or [React Server Components](/guides/server-components). This will allow you to securely store and use a client secret to access the provider's token endpoint.
## Examples
The following examples show how to use the `AuthSession` API to authenticate with a few popular providers.
<Box
  name="GitHub"
  createUrl="https://github.com/settings/developers"
  image={ASSETS.github}
>
| Website                                                   | Provider  | PKCE      | Auto Discovery |
| --------------------------------------------------------- | --------- | --------- | -------------- |
| [Get Your Config](https://github.com/settings/developers) | OAuth 2.0 | Supported | Not Available  |
- Provider only allows one redirect URI per app. You'll need an individual app for every method you want to use:
  - Standalone / development build: `com.your.app://*`
  - Web: `https://yourwebsite.com/*`
- The `redirectUri` requires two slashes (`://`).
- `revocationEndpoint` is dynamic and requires your `config.clientId`.
```tsx GitHub Auth Example
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
  authorizationEndpoint: 'https://github.com/login/oauth/authorize',
  tokenEndpoint: 'https://github.com/login/oauth/access_token',
  revocationEndpoint: 'https://github.com/settings/connections/applications/<CLIENT_ID>',
};
export default function App() {
  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: 'CLIENT_ID',
      scopes: ['identity'],
      redirectUri: makeRedirectUri({
        scheme: 'your.app'
      }),
    },
    discovery
  );
  useEffect(() => {
    if (response?.type === 'success') {
      const { code } = response.params;
    }
  }, [response]);
  return (
    <Button
      disabled={!request}
      title="Login"
      onPress={() => {
        promptAsync();
      }}
    />
  );
}
```
</Box>
<Box
  name="Okta"
  createUrl="https://developer.okta.com/signup"
  image={ASSETS.okta}
>
| Website                                                      | Provider | PKCE      | Auto Discovery |
| ------------------------------------------------------------ | -------- | --------- | -------------- |
| [Sign-up](https://developer.okta.com/signup/) > Applications | OpenID   | Supported | Available      |
- You cannot define a custom `redirectUri`, Okta will provide you with one.
```tsx Okta Auth Example
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
export default function App() {
  // Endpoint
  const discovery = useAutoDiscovery('https://<OKTA_DOMAIN>.com/oauth2/default');
  // Request
  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: 'CLIENT_ID',
      scopes: ['openid', 'profile'],
      redirectUri: makeRedirectUri({
        native: 'com.okta.<OKTA_DOMAIN>:/callback',
      }),
    },
    discovery
  );
  useEffect(() => {
    if (response?.type === 'success') {
      const { code } = response.params;
    }
  }, [response]);
  return (
    <Button
      disabled={!request}
      title="Login"
      onPress={() => {
        promptAsync();
      }}
    />
  );
}
```
</Box>
{/* End Okta */}
## Redirect URI patterns
Here are a few examples of some common redirect URI patterns you may end up using.
### Standalone/development build
> `yourscheme://path`
In some cases there will be anywhere between 1 to 3 slashes (`/`).
- **Environment:**
  - Bare workflow
    - `npx expo prebuild`
  - Standalone builds in the App or Play Store or testing locally
    - Android: `eas build` or `npx expo run:android`
    - iOS: `eas build` or `npx expo run:ios`
- **Create:** Use `AuthSession.makeRedirectUri({ native: '<YOUR_URI>' })` to select native when running in the correct environment.
  - `your.app://redirect` -> `makeRedirectUri({ scheme: 'your.app', path: 'redirect' })`
  - `your.app:///` -> `makeRedirectUri({ scheme: 'your.app', isTripleSlashed: true })`
  - `your.app:/authorize` -> `makeRedirectUri({ native: 'your.app:/authorize' })`
  - `your.app://auth?foo=bar` -> `makeRedirectUri({ scheme: 'your.app', path: 'auth', queryParams: { foo: 'bar' } })`
  - `exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version]` -> `makeRedirectUri()`
  - This link can often be created automatically but we recommend you define the `scheme` property at least. The entire URL can be overridden in apps by passing the `native` property. Often this will be used for providers like Google or Okta which require you to use a custom native URI redirect. You can add, list, and open URI schemes using `npx uri-scheme`.
  - If you change the `expo.scheme` after ejecting then you'll need to use the `expo apply` command to apply the changes to your native project, then rebuild them (`yarn ios`, `yarn android`).
- **Usage:** `promptAsync({ redirectUri })`
## Improving user experience
The "login flow" is an important thing to get right, in a lot of cases this is where the user will _commit_ to using your app again. A bad experience can cause users to give up on your app before they've really gotten to use it.
Here are a few tips you can use to make authentication quick, easy, and secure for your users:
### Warming the browser
On Android you can optionally warm up the web browser before it's used. This allows the browser app to pre-initialize itself in the background. Doing this can significantly speed up prompting the user for authentication.
```tsx
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
function App() {
  useEffect(() => {
    WebBrowser.warmUpAsync();
    return () => {
      WebBrowser.coolDownAsync();
    };
  }, []);
  // Do authentication ...
}
```
### Implicit login
Because there was no secure way to do this to store client secrets in your app bundle, historically, many providers have offered an "Implicit flow" which enables you to request an access token without the client secret. **This is no longer recommended** due to inherent security risks, including the risk of access token injection. Instead, most providers now support the authorization code with PKCE (Proof Key for Code Exchange) extension to securely exchange an authorization code for an access token within your client app code. Learn more about [transitioning from Implicit flow to authorization code with PKCE](https://oauth.net/2/grant-types/implicit/).
`expo-auth-session` still supports Implicit flow for legacy code purposes. Below is an example implementation of the Implicit flow.
```tsx
import { useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
  authorizationEndpoint: 'https://accounts.spotify.com/authorize',
};
function App() {
  const [request, response, promptAsync] = useAuthRequest(
    {
      responseType: ResponseType.Token,
      clientId: 'CLIENT_ID',
      scopes: ['user-read-email', 'playlist-modify-public'],
      redirectUri: makeRedirectUri({
        scheme: 'your.app'
      }),
    },
    discovery
  );
  useEffect(() => {
    if (response && response.type === 'success') {
      const token = response.params.access_token;
    }
  }, [response]);
  return <Button disabled={!request} onPress={() => promptAsync()} title="Login" />;
}
```
### Storing data
On native platforms such as Android and iOS, you can secure things like access tokens locally using a library called [`expo-secure-store`](/versions/latest/sdk/securestore) (This is different to `AsyncStorage` which is not secure). It provides native access to encrypted [`SharedPreferences`](https://developer.android.com/training/basics/data-storage/shared-preferences.html) on Android and [keychain services](https://developer.apple.com/documentation/security/keychain_services) on iOS and . There is no web equivalent to this functionality.
You can store your authentication results and rehydrate them later to avoid having to prompt the user to login again.
```tsx
import * as SecureStore from 'expo-secure-store';
const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKey';
function App() {
  const [, response] = useAuthRequest({});
  useEffect(() => {
    if (response && response.type === 'success') {
      const auth = response.params;
      const storageValue = JSON.stringify(auth);
      if (Platform.OS !== 'web') {
        // Securely store the auth on your device
        SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, storageValue);
      }
    }
  }, [response]);
  // More login code...
}
```


## Using Hermes Engine

A guide on configuring Hermes for both Android and iOS in an Expo project.

[Hermes](https://hermesengine.dev/) is a JavaScript engine optimized for React Native. By compiling JavaScript into bytecode ahead of time, Hermes can improve your app start-up time. The binary size of Hermes is also smaller than other JavaScript engines, such as JavaScriptCore (JSC). It also uses less memory at runtime, which is particularly valuable on lower-end Android devices.
## Support
The Hermes engine is the default JavaScript engine used by Expo and it is fully supported across all Expo tooling.
### Switch JavaScript engine on a specific platform
You may want to use Hermes on one platform and JSC on another. One way to do this is to set the `"jsEngine"` to `"hermes"` at the top level in app config and then override it with `"jsc"` under the `"ios"` key. You may alternatively prefer to explicitly set `"hermes"` on just the `"android"` key in this case.
```json app.json
{
  "expo": {
    "jsEngine": "hermes",
    "ios": {
      "jsEngine": "jsc"
    }
  }
}
```
## Publish updates
Publishing updates with `eas update` and `npx expo export` will generate Hermes bytecode bundles and their source maps.
Note that the Hermes bytecode format may change between different Hermes versions &mdash; an update produced for a specific version of Hermes will not run on a different version of Hermes. Starting from Expo SDK 46 (React Native 0.69), [Hermes is bundled within React Native](https://reactnative.dev/architecture/bundled-hermes). Updating React Native version or Hermes version can be thought of in the same way as updating any other native module. So if you update the `react-native` version you should also update the `runtimeVersion` in **app.json**. If you don't do this, your app may crash on launch because the update may be loaded by an existing binary that uses an older Hermes version that is incompatible with the updated bytecode format. See [`runtimeVersion`](/eas-update/runtime-versions/) for more information.
## JavaScript debugger
To debug JavaScript code running with Hermes, you can start your project with `npx expo start` then press <kbd>j</kbd> to open the debugger in Google Chrome or Microsoft Edge. The developer menu of development builds and Expo Go also have the **Open JS Debugger** option to do the same.
Alternatively, you can use the JavaScript inspector by opening [Google Chrome DevTools manually](https://reactnative.dev/docs/other-debugging-methods#remote-javascript-debugging-deprecated)
### Troubleshooting
> **warning** `No compatible apps connected. JavaScript Debugging can only be used with the Hermes engine.` when opening the debugger.
- Make sure you [set up Hermes in the `jsEngine` field](#setup).
- If your app is built by `eas build`, `npx expo run:android` or `npx expo run:ios`, make sure it is a debug build.
- Internally, the app will establish a WebSocket connection, make sure your app is connected to the development server.
  - Try to reload the app by pressing <kbd>r</kbd> in the Expo CLI Terminal UI.
  - Test debugging availability by running the command: `curl http://127.0.0.1:8081/json/list` (adjust the `127.0.0.1:8081` to match your dev server URL). The HTTP response should be an array, as shown below. If it is an empty response, add either the `--localhost` or `--tunnel` flag to the `npx expo start` command.
  ```json
  [
    {
      "id": "0-2",
      "description": "host.exp.Exponent",
      "title": "Hermes ABI47_0_0React Native",
      "faviconUrl": "https://react.dev/favicon.ico",
      "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D2",
      "type": "node",
      "webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=0&page=2",
      "vm": "Hermes"
    },
    {
      "id": "0--1",
      "description": "host.exp.Exponent",
      "title": "React Native Experimental (Improved Chrome Reloads)",
      "faviconUrl": "https://react.dev/favicon.ico",
      "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D0%26page%3D-1",
      "type": "node",
      "webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=0&page=-1",
      "vm": "don't use"
    }
  ]
  ```
### Can I use Remote Debugging with Hermes?
One of the many limitations of [remote debugging](/more/glossary-of-terms/#remote-debugging) is that it does not work with modules built on top of [JSI](https://github.com/react-native-community/discussions-and-proposals/issues/91), such as [`react-native-reanimated`](https://github.com/software-mansion/react-native-reanimated) version 2 or higher.
Hermes supports [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/v8/) to debug JavaScript in place by connecting to the engine running on the device, as opposed to remote debugging, which executes JavaScript within a desktop Chrome tab. Hermes apps use this debugging technique automatically when you open the debugger in Expo Go or a development build.


## iOS Developer Mode

Learn how to enable iOS Developer Mode setting on iOS 16 and above to run internal distribution builds and local development builds.

> This does not apply to builds signed using enterprise provisioning or to any builds installed on an iOS Simulator.
Devices running iOS 16 and above need to enable OS-level **Developer Mode** setting before they can run [internal distribution](/build/internal-distribution) builds (including those built with EAS) or local development builds after installing them on the device.
There are two ways you can enable Developer Mode on your device:
- Directly on an iOS device
- By connecting an iOS device with a Mac that has Xcode installed
## Prerequisites
The instructions specified below need to be followed once per device.
## Enable Developer Mode
### Directly on an iOS device
To follow the steps below, **install your development build on your device before enabling the Developer Mode.** When the build is created, follow the instructions on the EAS dashboard to install it on your iOS device.
Step 1: 
Once the build is installed on your device, press the app icon. This will open an alert asking you to enable Developer Mode. Press **OK**.
Step 2: 
Go to the Settings app, and navigate to **Privacy & Security** > **Developer Mode**.
Step 3: 
Enable the toggle. You will receive a prompt from iOS to restart your device. Press **Restart**.
Step 4: 
After the device restarts, unlock your device. A system alert should appear. Press **Turn On** and then, when prompted, enter your device's passcode.
Developer Mode is now enabled. You can now interact with your internal distribution builds and local development builds.
You can turn off Developer Mode at any time. However, you'll need to repeat this same process to re-enable it.
### Connect an iOS device with a Mac
> **Note:** Xcode must be installed on the Mac device before following the steps below.
You don't need to install the development build on your iOS device first to enable Developer Mode by connecting it to a Mac. You can:
Step 1: 
Connect your iOS device to a Mac using a USB cable. Press **Trust** on your iOS device when **Trust This Computer?** alert is prompted.
Step 2: 
Open Xcode, and from the menu bar, navigate to **Window** > **Devices and Simulators**.
Under **Devices**, you'll see a warning "Previous preparation error: Developer Mode disabled" with instructions on enabling Developer Mode on the iOS device.
Step 3: 
On the iOS device, open **Settings** > **Privacy & Security** > **Developer Mode**.
Enable the toggle. You will receive a prompt from iOS to restart your device. Press **Restart**.
Step 4: 
After the device restarts, unlock your device. A system alert should appear. Press **Turn On**, and enter your device's passcode when prompted.
Developer Mode is now enabled. You can now interact with your internal distribution builds and local development builds.
You can turn off Developer Mode at any time. However, you'll need to repeat this same process to re-enable it.


## Expo Vector Icons

Learn how to use various types of icons in your Expo app, including react native vector icons, custom icon fonts, icon images, and icon buttons.

Not every app needs to use emojis for icons. You can use a popular icon set through an icon font such as FontAwesome, Glyphicons, or Ionicons, or choose PNGs from [The Noun Project](https://thenounproject.com/). This guide explains various ways to use icons in your Expo app.
## `@expo/vector-icons`
The `@expo/vector-icons` library is installed by default on the template project using `npx create-expo-app` and is part of the `expo` package. It is built on top of [`react-native-vector-icons`](https://github.com/oblador/react-native-vector-icons) and uses a similar API. It includes popular icon sets you can browse at [icons.expo.fyi](https://icons.expo.fyi).
The component loads the `Ionicons` font and renders a checkmark icon in the following example:
<SnackInline label='Vector icons' dependencies={['@expo/vector-icons']}>
```jsx collapseHeight=350
import { View, StyleSheet } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function App() {
  return (
    <View style={styles.container}>
      <Ionicons name="checkmark-circle" size={32} color="green" />
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
```
> As with [any custom font](/develop/user-interface/fonts/#use-a-local-font-file) in Expo, you can preload icon fonts before rendering your app. The font object is available as a static property on the font component. So, in the case above, it is `Ionicons.font`, which evaluates to `{ionicons: require('path/to/ionicons.ttf')}`.
## Custom icon fonts
To use a custom icon font, first import it into your project. Only after the font has loaded, can you create an Icon set. [Learn more about loading custom fonts](/develop/user-interface/fonts/#handle-expovector-icons-initial-load).
`@expo/vector-icons` exposes three methods to help you create an icon set:
### `createIconSet`
The `createIconSet` method returns a custom font based on the `glyphMap` where the key is the icon name and the value is either a UTF-8 character or its character code.
In the example below, the `glyphMap` object is defined and then passed as the first argument to the `createIconSet` method. The second argument `fontFamily` is the name of the font (not the filename). Optionally, you can pass the third argument for Android support, which is the custom font file name.
```jsx createIconSet example
import createIconSet from '@expo/vector-icons/createIconSet';
const glyphMap = { 'icon-name': 1234, test: 'â' };
const CustomIcon = createIconSet(glyphMap, 'fontFamily', 'custom-icon-font.ttf');
export default function CustomIconExample() {
  return <CustomIcon name="icon-name" size={32} color="red" />;
}
```
### `createIconSetFromIcoMoon`
The `createIconSetFromIcoMoon` method is used to create a custom font based on an [IcoMoon](https://icomoon.io/) config file. You have to save the **selection.json** and **.ttf** in your project, preferably in the **assets** directory, and then load the font using either `useFonts` hook or `Font.loadAsync` method from `expo-font`.
See the example below that uses the `useFonts` hook to load the font:
<SnackInline
label='Icomoon Icons'
files={{
    'assets/icomoon/icomoon.ttf': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/71ce651cbddbee5366aef87c456a80bb',
    'assets/icomoon/selection.json': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/a06aa5b6e7eb1df1fa1c8d06d4ab8463'
  }}
dependencies={['@expo/vector-icons', 'expo-font']}>
```jsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { useFonts } from 'expo-font';
import createIconSetFromIcoMoon from '@expo/vector-icons/createIconSetFromIcoMoon';
const Icon = createIconSetFromIcoMoon(
  require('./assets/icomoon/selection.json'),
  'IcoMoon',
  'icomoon.ttf'
);
export default function App() {
  const [fontsLoaded] = useFonts({
    IcoMoon: require('./assets/icomoon/icomoon.ttf'),
  });
  if (!fontsLoaded) {
    return null;
  }
  return (
    <View style={styles.container}>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
```
### `createIconSetFromFontello`
The `createIconSetFromFontello` method is used to create a custom font based on a [Fontello](http://fontello.com/) config file. You have to save the **config.json** and **.ttf** somewhere convenient in your project, preferably in the **assets** directory, and then load the font using either `useFonts` hook or `Font.loadAsync` method from `expo-font`.
It follows a similar configuration as `createIconSetFromIcoMoon` as shown in the example:
```js Fontello Icons
// Import the createIconSetFromFontello method
import createIconSetFromFontello from '@expo/vector-icons/createIconSetFromFontello';
// Import the config file
import fontelloConfig from './config.json';
// Both the font name and files exported from Fontello are most likely called "fontello".
// Ensure this is the `fontname.ttf` and not the file path.
const Icon = createIconSetFromFontello(fontelloConfig, 'fontello', 'fontello.ttf');
```
## Button component
You can create an Icon Button using the `Font.Button` syntax where the `Font` is the icon set that you import from `@expo/vector-icons`.
In the example below, a login button uses the `FontAwesome` icon set. Notice that the `FontAwesome.Button` component accepts props to handle action when a button is pressed and can wrap the text of the button.
<SnackInline label='Icon Button Component' dependencies={['@expo/vector-icons']}>
```jsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import FontAwesome from '@expo/vector-icons/FontAwesome';
export default function App() {
  const loginWithFacebook = () => {
    console.log('Button pressed');
  };
  return (
    <View style={styles.container}>
      <FontAwesome.Button name="facebook" backgroundColor="#3b5998" onPress={loginWithFacebook}>
        Login with Facebook
      </FontAwesome.Button>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
```
### Properties
Any [`Text`](http://reactnative.dev/docs/text), [`TouchableHighlight`](http://reactnative.dev/docs/touchablehighlight), or [`TouchableWithoutFeedback`](http://reactnative.dev/docs/touchablewithoutfeedback) property in addition to these:
| Prop              | Description                                                                                                                                      | Default             |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- |
| `color`           | Text and icon color, use `iconStyle` or nest a `Text` component if you need different colors.                                                    | `white`             |
| `size`            | Icon size.                                                                                                                                       | `20`                |
| `iconStyle`       | Styles applied to the icon only, good for setting margins or a different color. _Note: use `iconStyle` for margins or expect unstable behavior._ | `{marginRight: 10}` |
| `backgroundColor` | Background color of the button.                                                                                                                  | `#007AFF`           |
| `borderRadius`    | Border radius of the button, set to `0` to disable.                                                                                              | `5`                 |
| `onPress`         | A function called when the button is pressed.                                                                                                    | _None_              |


## Localization

Learn about getting started and configuring localization in an Expo project using expo-localization.

If you want your app to be easy to use for users who speak different languages or come from different cultures, you should localize it. Localizing an app makes it adapt to the locale of the user's device. The app will show translations and currencies that the user knows and understands. Numbers, lists, and more will be formatted in a way that the user is used to.
This guide uses the `expo-localization` library for accessing user language settings and adding support for multiple languages. It uses `i18n-js` as an example to add multi-language support.
## Getting the user's language
Use the [`expo-localization`](/versions/latest/sdk/localization/) library to get the user's current language. Install the package by running the following command:
```sh
$ npx expo install expo-localization
```
Then, you will be able to access localization methods and data in your app:
```tsx
import { getLocales } from 'expo-localization';
const deviceLanguage = getLocales()[0].languageCode;
```
The `getLocales` method returns the current locale based on the system settings of the device. On newer Android and iOS versions, app language can be set per app, so you usually don't need to build a custom UI to allow users to change the current locale inside of your app.
Sometimes, it makes sense to build a UI to allow the user to set other localization preferences on a per-app basis. As a general rule, you should allow the user to change the following:
- Localized units if your app makes at least a moderate use of them (such as metric/imperial measurements, currency, temperature, and more)
- Other preferences if there's no API to get the default value on platforms you want to support (check [`expo-localization`](/versions/latest/sdk/localization) API documentation for details)
### Enabling per-app language selection via system settings
Both Android and iOS allow users to choose a preferred language for individual apps via the system settings. To support this feature, your app must declare its supported locales to the system.
To do so, use the [`expo-localization`](/versions/latest/sdk/localization/#installation) config plugin and pass the `supportedLocales` property to the `expo-localization` config plugin.
You can either provide an array of supported locales directly, or use the `supportedLocales.ios` and `supportedLocales.android` fields to specify platform-specific values:
```json app.json
{
  "expo": {
    "plugins": [
      [
        "expo-localization",
        {
          "supportedLocales": {
            "ios": ["en", "ja"],
            "android": ["en", "ja"]
          }
        }
      ]
    ]
  }
}
```
> On Android, refer to the [locale naming guidelines](https://developer.android.com/guide/topics/resources/app-languages#locale-names) and the [list of most commonly used locales](https://developer.android.com/guide/topics/resources/app-languages#sample-config).
>
> On iOS, use the language name or ISO language designator.
## Translating an app
Creating and managing translations quickly becomes a large task. You can handle translations manually, but it's best to use a library to handle this for you.
Let's make the app support English and Japanese. To achieve that, this guide uses the `i18n-js` package:
```sh
$ npx expo install i18n-js
```
Take a look at [other translation libraries](#other-translation-libraries) to find one that best suits your needs.
Then, configure the languages for your app:
```tsx
import { getLocales } from 'expo-localization';
import { I18n } from 'i18n-js';
// Set the key-value pairs for the different languages you want to support.
const i18n = new I18n({
  en: { welcome: 'Hello' },
  ja: { welcome: 'ããã«ã¡ã¯' },
});
// Set the locale once at the beginning of your app.
i18n.locale = getLocales().at(0)?.languageCode ?? 'en'; // you can also do getLocales()[0].languageCode ?? 'en'
console.log(i18n.t('welcome'));
```
Now, you can use the `i18n.t` function to translate strings throughout your application.
You can refrain from localizing text for certain things, for example, names. In this case, you can define them _once_ in your default language and reuse them with `i18n.enableFallback = true;`.
On Android, when a user changes the device's language, the app will not reset. You can use the [`AppState`](https://reactnative.dev/docs/appstate#basic-usage) API to listen for changes to the app's state and call the `getLocales()` function each time the app's state changes.
On iOS, when a user changes the device's language, the app will reset. This means you can set the language once without updating any of your React components to account for the language changes.
### Complete example
#### Localization
```tsx
import { View, StyleSheet, Text } from 'react-native';
import { getLocales } from 'expo-localization';
import { I18n } from 'i18n-js';
// Set the key-value pairs for the different languages you want to support.
const translations = {
  en: { welcome: 'Hello', name: 'Charlie' },
  ja: { welcome: 'ããã«ã¡ã¯' },
};
const i18n = new I18n(translations);
// Set the locale once at the beginning of your app.
i18n.locale = getLocales()[0].languageCode ?? 'en';
// When a value is missing from a language it'll fall back to another language with the key present.
i18n.enableFallback = true;
// To see the fallback mechanism uncomment the line below to force the app to use the Japanese language.
// i18n.locale = 'ja';
export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>
        {i18n.t('welcome')} {i18n.t('name')}
      </Text>
      <Text>Current locale: {i18n.locale}</Text>
      <Text>Device locale: {getLocales()[0].languageCode}</Text>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
    flex: 1,
  },
  text: {
    fontSize: 20,
    marginBottom: 16,
  },
});
```
### Other translation libraries
This guide uses `i18n-js` as an example, but other libraries can also help you with the task. Creating translations is a major effort. Some pointers to consider when choosing a library:
- Integration with translation management tools for easier strings management and automations.
- Ability to provide context to strings, so that AI translation tools and/or human reviewers can understand the context of the string and provide better translations.
- Developer experience - usage in the context of React / JSX. Some libraries come with ESLint plugins and other tools to help in development.
- Don't worry about localizing dates or numbers - use standardized `Intl` APIs for that.
Here is a non-exhaustive list of other libraries you can consider:
- [Lingui](https://lingui.dev/) is a mature library with first-class React (including React Server Components (RSC)) support and integrates well with translation management tools.
- [fbtee](https://fbtee.dev/) is an internationalization framework for JavaScript and React designed to be powerful, flexible, and intuitive.
- [React i18next](https://react.i18next.com/) is a stable, well-maintained library based on `i18next`.
### Translating app metadata
If you plan on shipping your app to different countries or regions or want it to support various languages, you can provide [localized](/versions/latest/sdk/localization) strings for things like the display name and system dialogs. This is easily set up [in the app config](/workflow/configuration) file. First, set `ios.infoPlist.CFBundleAllowMixedLocalizations: true`, then provide a list of file paths to `locales`.
```json app.json
{
  "expo": {
    "ios": {
      "infoPlist": {
        "CFBundleAllowMixedLocalizations": true
      }
    },
    "locales": {
      "ja": "./languages/japanese.json"
    }
  }
}
```
The keys provided to `locales` should be the [language identifier](https://developer.apple.com/documentation/xcode/choosing-localization-regions-and-scripts), made up of a [2-letter language code](https://www.loc.gov/standards/iso639-2/php/code_list.php) of your desired language, with an optional region code (for example, `en-US` or `en-GB`), and the value should point to a JSON file that looks something like below:
```json japanese.json
{
  "ios": {
    "CFBundleDisplayName": "ããã«ã¡ã¯",
    "NSContactsUsageDescription": "æ¥æ¬èªã®ãããã®è¨è",
    "Localizable.strings": {
      "HELLO_NOTIFICATION_KEY": "ããã«ã¡ã¯ä¸ç"
    }
  },
  "android": {
    "app_name": "ããã«ã¡ã¯",
    "HELLO_NOTIFICATION_KEY": "ããã«ã¡ã¯ä¸ç"
  }
}
```
Now, the display name of your app is set to `ããã«ã¡ã¯` whenever it's installed on a device with the language set to Japanese.
In SDK 55 and later, there is an iOS-only option to specify a `Localizable.strings` object whose entries are used to create native localization files. The entries can be used in [iOS localized notifications](https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification#Localize-your-alert-messages).
## Enabling RTL support
Several regions around the world write text from right to left. If you want to localize your app, so it looks as expected in RTL languages, you need to make sure your app handles these layout and text direction changes accordingly.
To enable RTL support, use the [`expo-localization`](/versions/latest/sdk/localization/#installation) config plugin and enable `extra.supportsRTL` property in app config:
```json app.json
{
  "expo": {
    "extra": {
      "supportsRTL": true
    },
    "plugins": ["expo-localization"]
  }
}
```
This enables RTL when your app is loaded in Expo Go, in Expo dev Client, and in applications built using EAS Build or `npx expo prebuild`.
When an application starts, Expo checks if the current device locale should render in RTL layout to look correctly. For example, an app marked to support RTL in the app config file will render in RTL mode in Hebrew or Arabic locale.
### Forcing RTL layout
You can also force the RTL layout for testing or for applications that are only localized for RTL locales by enabling `extra.forcesRTL` property in the app config:
```json app.json
{
  "expo": {
    "extra": {
      "supportsRTL": true,
      "forcesRTL": true
    },
    "plugins": ["expo-localization"]
  }
}
```
Note: Dynamically overriding RTL settings
---
If you want to override the default RTL detection from your application code dynamically, you cannot use the static configuration in app config. Instead, apply these changes dynamically from your application code.
This does not work in Expo Go, as Expo Go resets RTL preferences when opening the launcher or individual projects.
#### Overriding RTL settings
```tsx
import { Text, View, StyleSheet, I18nManager, Platform } from 'react-native';
import Constants from 'expo-constants';
import * as Updates from 'expo-updates';
export default function App() {
  const shouldBeRTL = true;
  if (shouldBeRTL !== I18nManager.isRTL && Platform.OS !== 'web') {
    I18nManager.allowRTL(shouldBeRTL);
    I18nManager.forceRTL(shouldBeRTL);
    Updates.reloadAsync();
  }
  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>{I18nManager.isRTL ? ' RTL' : ' LTR'}</Text>
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    padding: 8,
  },
  paragraph: {
    fontSize: 18,
    fontWeight: 'bold',
    textAlign: 'left',
    width: '50%',
    backgroundColor: 'pink',
  },
});
```
---
## Making an app behave correctly on RTL locales
### Layouts and views
You don't need to manually adjust `<View>` styling properties based on locale. You can use properties like `justifyContent`, `alignItems`, and others. Their property values change behavior as required.
- On LTR locales, `start` and `end` are the same as `left` and `right`.
- On RTL locales, `start` and `end` are the same as `right` and `left`.
> **info** For more details on how RTL works in React Native check out the React Native [blog article](https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps) introducing RTL support.
#### Web support
Web support for RTL layouts requires no app config changes.
Expo uses `react-native-web` for running Expo projects in the browser. To make `react-native-web` automatically adapt to locale direction, add a `dir` property to your root `<View>` component.
```tsx App.tsx
import { View } from 'react-native';
import { getLocales } from 'expo-localization';
// ...
return <View dir={getLocales()[0].textDirection || 'ltr'}>...</View>;
```
> **warning** `textDirection` is not available on Firefox and older browser versions. [Detect it manually](https://stackoverflow.com/a/15726039) if needed.
### Text alignment
The React Native's `textDirection` property does not accept `start` or `end` values that you can use in flex properties. Instead, `left` effectively works as `start` (aligns to the left on LTR and the right on RTL), and `right` works as `end`.
However, the default unset value of `textDirection` property signifies the actual left (aligns to the left both on LTR and RTL). This means each `<Text>` tag should have the `textDirection: left` or `textDirection: right` style set if you want it to be aligned correctly.
It's best to define this style in your custom reusable `<Text>` component that you can then import everywhere you need to render text strings.
```tsx MobileText.tsx
import { Text as RNText, TextProps as RNTextProps } from 'react-native';
const MobileText = (props: RNTextProps) => {
  return <RNText style={{ textAlign: 'left', ...props.style }} {...props} />;
};
export default MobileText;
```
#### Web support
For each text tag, you need to add the `lang` property with the current locale identifier. It's best to define this style in a custom reusable component.
```tsx WebText.tsx
import { getLocales } from 'expo-localization';
const deviceLanguage = getLocales()[0].languageCode;
const WebText = (props: RNTextProps) => {
  return <RNText lang={deviceLanguage} {...props} />;
};
export default WebText;
```
You can then pick either the mobile or web Text component based on the current platform.
```tsx Text.tsx
const Text = Platform.OS === 'web' ? WebText : MobileText;
export default Text;
```
### Selecting assets based on locale direction
If you need to use different icons for LTR/RTL or change styles based on this setting, you can use [`I18nManager.isRTL`](https://reactnative.dev/docs/next/i18nmanager#isrtl) to get the current layout direction.
```tsx
import { I18nManager } from 'react-native';
const isRTL = I18nManager.isRTL;
```
## Locale settings and units
Expo provides the `expo-localization` library to allow you to read the user's locale and other preferences. You can use synchronous `getLocales()` and `getCalendars()` methods to get the current locale settings of the user device:
- `getLocales()` returns a list of locales based on the order in which the user prefers them. There will always be at least one locale in the list.
- `getCalendars()` returns a list of calendars based on the order in which the user prefers them. There will always be at least one calendar on the list.
```ts
import { getLocales, getCalendars } from 'expo-localization';
const {
  languageTag,
  languageCode,
  textDirection,
  digitGroupingSeparator,
  decimalSeparator,
  measurementSystem,
  currencyCode,
  currencySymbol,
  regionCode,
} = getLocales()[0];
const { calendar, timeZone, uses24hourClock, firstWeekday } = getCalendars()[0];
```
Note: Limitations
---
There are a few limitations to keep in mind when relying on auto-detected locale preferences from `expo-localization`.
- There is yet to be a way to read temperature units from user preferences. On Android, you can use a lookup table based on locale. However, on iOS, the user can change it in device preferences.
- Some properties can be null when they are unavailable on the current platform.
---
## Intl API
If you're using Hermes in your app, you can use the [`Intl`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) API on all platforms.
It provides a set of utilities you can use to format lists, dates, numbers, monetary amounts, units, plural forms, and more.
If you pass `default` as the locale string, the `Intl` API will use the device's locale, so you don't need to rely on `expo-localization` to get the current locale (such as `"en-US"`).
```ts
new Intl.NumberFormat('default', { style: 'currency', currency: 'EUR' }).format(5.0);
```
> You can use `Intl` APIs to format strings and values once you know what the user expects to see.
>
> `Intl` APIs do not provide information about the device or current locale, so you can't use the `Intl` APIs to get current locale units, currencies, or measurement systems.
>
> For this you need to use `expo-localization`, JS code on the web, or third-party or custom native code on Android and iOS.


## Configure JS engines

A guide on configuring JS engines for both Android and iOS in an Expo project.

JavaScript engines execute your application code and provide various features such as memory management, optimization, and error handling. By default, Expo projects use [Hermes](https://hermesengine.dev/) as the JavaScript engine. Switching to other engines, such as JSC or V8, for Android and iOS platforms is possible. However, not for the web because the JavaScript engine is included in the web browser.
## Configuring the `jsEngine` through app config
> **warning** Changing the JS engine is unavailable in Expo Go from SDK 52 (only the Hermes engine is available). A [development build](/develop/development-builds/introduction/) is required to use this customization.
We recommend Hermes because it is purpose built and optimized for React Native apps, and it has the best debugging experience. If you are familiar with the tradeoffs of different JavaScript engines and would like to change away from Hermes, the [`jsEngine`](/versions/latest/config/app/#jsengine) field inside [app config](/workflow/configuration/) allows you to specify the JavaScript engine for your app. The default value is `hermes`.
If you want to use JSC instead, set the `jsEngine` field in the app config:
```json app.json
{
  "expo": {
    "jsEngine": "jsc"
  }
}
```
Note: Usage in bare React Native projects
---
To change the JavaScript engine in a bare React Native project, update theÂ `expo.jsEngine` value inÂ **android/gradle.properties** and **ios/Podfile.properties.json**.
---
It's important to highlight that changing the JS engine will require you to recompile development builds with `eas build` to work properly.
## Using the V8 engine
To use the V8 engine, you need to install [`react-native-v8`](https://github.com/Kudo/react-native-v8), an opt-in package that adds V8 runtime support for React Native. You can install it by running the following command:
```sh
$ npx expo install react-native-v8 v8-android-jit
```
Make sure to remove the `jsEngine` field from the app config.
Note: Usage with Expo Prebuild
---
Run `npx expo prebuild -p android --clean` after the installation to prebuild again.
---
## Switch JavaScript engine on a specific platform
To use a different engine on one specific platform, you can set the `"jsEngine"` value at the top level and then override it with a different value under the `"android"` or `"ios"` key. The value specified for the platform will take precedence over the common field.
```json app.json
{
  "expo": {
    "jsEngine": "hermes",
    "ios": {
      "jsEngine": "jsc"
    }
  }
}
```


## Using Bun

A guide on using Bun with Expo and EAS.

[Bun](https://bun.sh/) is a JavaScript runtime and a drop-in alternative for [Node.js](https://nodejs.org/en). In Expo projects, Bun can be used to install npm packages and run Node.js scripts. The benefits of using Bun are faster package installation than npm, pnpm, or Yarn and [at least 4x faster startup time compared to Node.js](https://bun.sh/docs#design-goals), which gives a huge boost to your local development experience.
## Prerequisites
> **Note:** While Bun replaces Node.js for most use cases in your project, at this time, a [Node.js LTS version](https://nodejs.org/) is still required to be installed for the `bun create expo` and `bun expo prebuild` commands. These commands use `npm pack` to download project templates.
To create a new app using Bun, [install Bun on your local machine](https://bun.sh/docs/installation#installing).
## Start a new Expo project with Bun
To create a new project, run the command:
```sh
$ bun create expo-app my-app
```
You can also run any **package.json** script with `bun run`:
```sh
$ bun run ios
```
To install any Expo library, you can use `bun expo install`:
```sh
$ bun expo install expo-audio
```
## Use Bun for EAS builds
EAS decides which package manager to use based on the lockfile in your codebase. If you want EAS to use Bun, run `bun install` in your codebase which will create a **bun.lockb** (the Bun lockfile). As long as this lockfile is in your codebase, Bun will be used as the package manager for your builds. Make sure to delete any lockfiles generated by other package managers.
### Customize Bun version on EAS
Bun is installed by default when using EAS. See the [Android server images](/build-reference/infrastructure/#android-server-images) and [iOS server images](/build-reference/infrastructure/#ios-server-images) to learn which version of Bun is used by your build's image.
To use an [exact version of Bun](/eas/json/#bun) with EAS, add the version number in **eas.json** under the build profile's configuration. For example, the configuration below specifies Bun version `1.0.0` for the `development` build profile:
```json eas.json
{
  "build": {
    "development": {
      "bun": "1.0.0"
    }
  }
}
```
## Trusted dependencies
Unlike other package managers, Bun does not automatically execute lifecycle scripts from installed libraries, as this is considered a security risk. However, if a package you are installing has a `postinstall` script that you want to run, you have to explicitly state that by including that library in your [`trustedDependencies`](https://bun.sh/guides/install/trusted) array in your **package.json**.
For example, if you install `packageA`, which has a dependency on `packageB` and `packageB` has a `postinstall` script, you must add `packageB` in your `trustedDependencies`.
To add a trusted dependency in your **package.json**, add:
```json package.json
"trustedDependencies": ["your-dependency"]
```
Then, remove your lockfile and re-install the dependencies:
```sh
$ rm -rf node_modules
$ rm bun.lockb
$ bun install
```
## Common errors
### EAS Build fails when using Sentry and Bun
If you're using `sentry-expo` or `@sentry/react-native`, these depend on `@sentry/cli`, which updates source maps to Sentry during your build. The `@sentry/cli` package has a `postinstall` script which needs to run for the "upload source maps" script to become available.
To fix this, add `@sentry/cli` to your [trusted dependencies](/#trusted-dependencies) array in **package.json**:
```json package.json
"trustedDependencies": ["@sentry/cli"]
```


## Edit rich text

Learn about current approaches to preview and edit rich text in React Native.

A lot of applications need to allow users to type in text. For example, if you want to build a messaging or social media app, you will probably rely heavily on text inputs. React Native has a built-in `<TextInput>` component to implement this without much effort for many simple cases.
However, sometimes you need to be more flexible. Think of long social media posts, note apps, or document editors. Ideally, you need to allow different text styles, lists, headings, embedded images, and more. This is called a rich text editor, and it's a difficult problem to solve everywhere, including in React Native.
There is currently no default solution for that in the React Native ecosystem. However, this guide explores some options and promising approaches, each with its own tradeoffs.
Video Tutorial: [Watch: How to implement a Rich Text Editor using DOM components](https://www.youtube.com/watch?v=CxORa1tXMjw)
## Render rich text
There are a lot of good options to display rich text:
- For markdown content, you can use a markdown renderer such as [`react-native-markdown-display`](https://www.npmjs.com/package/react-native-markdown-display) or another.
- For HTML content, you can use [`@expo/html-elements`](https://www.npmjs.com/package/@expo/html-elements) or a webview ([`react-native-webview`](/versions/latest/sdk/webview/)).
- To have a custom format and more control, you can take advantage of nesting `<Text>` components to render styles and layouts.
  ```jsx
  <TextInput>
    <Text>
      <Text style={{ fontWeight: 900 }}>Some bold text</Text>Some regular text
    </Text>
  </TextInput>
  ```
- You can also use [Expo Modules API](/modules/overview/) to write a custom renderer component with native platform primitives using third-party libraries such as [Markwon](https://github.com/noties/Markwon) on Android and `AttributedString` on iOS.
## Approaches to edit rich text
There are a few approaches to get rich text rendering to work. However, all have different limitations.
### Webview-based editors
While most React Native UI components wrap native platform primitives and are fast, performant, and **native feeling** as a result, the webview-based rich text editors use a different approach.
They wrap an existing rich text editor built for web with JavaScript inside a [`react-native-webview`](/versions/latest/sdk/webview/). It works on all platforms (Android, iOS, Web) and can take advantage of popular rich text editors available for the Web platform, but it has a performance and UX penalty.
You will not be able to use native UI components inside the editor. Any implementation of features like mentions or image embedding will duplicate features and require significant effort to implement.
### Existing webview-based React Native libraries
There are a couple of existing React Native libraries to allow rich-text editing. These are the easiest options to get started if you need a basic rich text editor with limited configuration and don't have strict performance or UX requirements:
- [`react-native-rich-editor`](https://github.com/wxik/react-native-rich-editor)
- [`react-native-cn-quill`](https://github.com/imnapo/react-native-cn-quill)
- [`@10play/tentap-editor`](https://github.com/10play/10Tap-Editor)
### Custom webview-based editor
If you need more configurability, you can build a similar library with an existing web-only editor. However, you have to handle the message passing and web implementation yourself. This gives you all the options that the underlying editor offers and lets you implement more features.
- [Quill](https://quilljs.com/)
- [lexical](https://github.com/facebook/lexical)
- [slate](https://github.com/ianstormtaylor/slate)
You will need to use message passing to pass text and `onChange` events to and from the webview. Since rich texts often end up long, it's better to model it as an uncontrolled component to prevent lag on each keystroke. Also, if you can avoid serializing and sending the entire state on each keystroke that also improves performance.
## Building on top of React Native TextInput
In this section, let's discuss if it is possible to have a feature complete rich text input for general purposes.
React Native allows nested `<Text>` components and allows them to be used as children of `<TextInput>` to render and edit styled text. It is synchronous with the new React Native architecture (the `onChange` event fires immediately after a new character is typed into the text input field).
Unfortunately, the `<TextInput>` component is built toward working with regular text, and it only returns a string in it's `onTextChange` callback. This is a significant limitation.
Here is a quick example. Let's start by rendering a text input with the following bold text:
```jsx
<TextInput>
  <Text>
    {/* The following will render a bold text in this format: **aa**aa */}
    <Text style={{ fontWeight: 900 }}>aa</Text>aa
  </Text>
</TextInput>
```
Then, let's append a fifth letter `a` to the text input. The position of your cursor should determine if the new letter is a part of the bold string or not. Unfortunately, the callback only returns `aaaaa`.
There is an additional `onSelectionChange` prop that can be used to get that information. However, it makes the task significantly harder. Inserting additional characters (such as newlines for lists or bullet points) also desynchronizes the selection.
There are some attempts to build such an editor, such as [`markdown-editor`](https://github.com/shakogegia/markdown-editor) (not actively maintained) and [`rn-text-editor`](https://github.com/amjadbouhouch/rn-text-editor) (in beta), but no widely used packages exist.
## Markdown editors with visible styling markers
If using a markdown to style text fulfills your requirement, you can render the markdown in a separate, non-editable view while editing. It is not complex to build on your own using any markdown renderer. You can also use a third-party library such as [`react-native-markdown-editor`](https://github.com/kunall17/react-native-markdown-editor).
This editing experience suits power users or programming/tech applications. You can also explore rendering rich text while showing markdown only in the selected chunk of text or other hybrid approaches.
## Native editors
You can use a native Android or iOS rich text editor wrapped into a React Native module. There are a few options:
- [`react-native-aztec`](https://github.com/WordPress/gutenberg/tree/trunk/packages/react-native-aztec)
- [`gutenberg-mobile`](https://github.com/wordpress-mobile/gutenberg-mobile)
- [`react-native-live-markdown`](https://github.com/Expensify/react-native-live-markdown)
- [`react-native-enriched`](https://github.com/software-mansion-labs/react-native-enriched)
You can also wrap any native rich text editor using [Expo Modules API](/modules/overview/), but if you use different ones on each platform, you need to unify their APIs and input formats.
> Rich text is usually represented using [abstract syntax trees](https://en.wikipedia.org/wiki/Abstract_syntax_tree). For example, a bullet list can be a node of type `bulleted-list` with several children of type `list-item`. You can convert both HTML and markdown to a suitable AST format.
There is also an effort to port the lexical editor to Android and iOS and provide a React Native wrapper, [track it here](https://github.com/facebook/lexical/discussions/2410).
## Summary
While there are many great options for showing rich text, there is no one-size-fits-all solution for rich text editing in React Native. There's no popular solution that is sufficient in most use cases. Further contribution from the community is needed to improve existing solutions or add new ones.
For now, you need to carefully choose between a more complex, native editor that offers more features but may be harder to maintain, or an editor built on top of react-native primitives. This depends on how core the feature is to your app, how many features you need in your editor, and how much effort you are willing to spend on building it out.


## Create app store assets

Learn how to create screenshots and previews for your app's store pages.

Before submitting your app to the Google Play Store and Apple App Store, you need to provide some assets for your store listing page. The goal of these images and videos is to give your prospective users an idea of what your app experience is going to be like.
You will need to upload app screenshots for both app stores. Even though they are called "app screenshots" on both stores, they _do_ have to include accurate visuals of your app. There is no rule citing that these images have to be screenshots taken on specific devices.
Both app stores have requirements on image format and size. However, within these restrictions, you can be creative. For example, a common approach is to design the assets using a design tool such as Figma and incorporate actual app screenshots (or designs) with supplementary messaging.
## Different approaches for creating "screenshots"
There are three commonly used approaches to creating your store screenshots. You can choose whichever approach works best for your app's needs and resources.
### Option 1: actual screenshots
The most straightforward option &mdash; open up your app on a real device and take screenshots.
**Pros**: Straightforward to do. Most accurate representation of your app.
**Cons**: Need to load the app on different devices for a full range of screenshots.
### Option 2: screenshots within a design
The majority of apps use this approach. It involves taking screenshots of the app (or in some cases, using existing designs instead of actual screenshots) and embedding them in the store assets along with appropriate messaging.
**Pros:** Allows to convey additional messaging within the assets.
**Cons:** The assets need to be created using a design program.
### Option 3: make it fancy
In this option, on the app store pages, you can incorporate elements of your app design and creative messaging to highlight your product.
**Pros:** You can make your store page creative and fun.
**Cons:** Need an experienced designer to create and maintain the assets.
## Google Play Store Asset Requirements
Google has specific requirements for the store asset format and dimensions, which are different from Apple's. For the most up-to-date specifications, see [official documentation](https://support.google.com/googleplay/android-developer/answer/9866151) for detailed requirements for your Google Play Store assets.
### App icon
Unlike on the Apple App Store, where the app icon is always taken automatically from the app bundle, on the Google Play Store, you must also upload a separate App Icon for your store listing.
### Feature graphic
A feature graphic must be provided to publish your Store Listing. It is a banner that is displayed at the top of your store listing page.
### Screenshots
You need to upload at least four screenshots to publish your app.
### Video (optional)
You can add one preview video to your Store Listing. The video needs to be uploaded to YouTube, and you can add it by entering a YouTube URL in the **preview video** field.
## iOS App Store Asset Requirements
For the iOS App Store, you can upload screenshots (images) and previews (videos). For each, Apple requires specific widths and heights. Make sure to reference the [Screenshot specifications from Apple](https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications/) for the exact sizing. Even a single pixel-off will mean you cannot submit the images.
### Screenshots
At a minimum, Apple requires you to upload screenshots for iPhone with the dynamic island (6.9 inch). You can optionally upload additional screenshots for other screen sizes. If specific screenshots are not provided, scaled down screenshots from the closest uploaded size will be used instead.
If your app runs on iPad, you must also provide one set of iPad screenshots (13 inch).
You can upload up to ten screenshots per localization. If your app is available in multiple languages and your screenshots include text, you should upload screenshots with the appropriate language for each localization.
Screenshots can be portrait or landscape.
### Preview (optional)
You can include An app preview video to demonstrate how your app works. You can add up to three app previews for each screen size.
See [App Preview Specifications](https://developer.apple.com/help/app-store-connect/reference/app-preview-specifications/) from Apple documentation for a summary of video size and format.
## Bare minimum
Below is the bare minimum needed to publish your apps.
### Play Store - Android
| Type            | Amount | Dimensions                                                          | Requirements                                        |
| --------------- | ------ | ------------------------------------------------------------------- | --------------------------------------------------- |
| App Icon        | 1      | 512 Ã 512                                                           | 32-bit PNG (with alpha); Maximum file size: 1024 KB |
| Feature Graphic | 1      | 1024 Ã 500                                                          | JPEG or 24-bit PNG (no alpha)                       |
| Screenshots     | 4-10   | minimum: 1024 Ã 500maximum width: 3840px9:16 aspect ratio | JPEG or 24-bit PNG (no alpha)                       |
### App Store - iPhone
| Type                                         | Amount | Dimensions (choose one)     | Requirements          |
| -------------------------------------------- | ------ | --------------------------- | --------------------- |
| Screenshots (iPhone with the dynamic island) | 2-10   | 1320 Ã 28681290 Ã 2796 | JPG or PNG (no alpha) |
### App Store - iPad
If your app also runs on an iPad, you need to supply additional screenshots.
| Type        | Amount | Dimensions (choose one)     | Requirements          |
| ----------- | ------ | --------------------------- | --------------------- |
| Screenshots | 2-10   | 2064 Ã 27522048 Ã 2732 | JPG or PNG (no alpha) |


## Local-first architecture with Expo

An introduction to the emerging local-first software movement, including links to relevant learning resources and tools.

> **info** This guide is a work in progress. If you have any feedback, [open an issue](https://github.com/expo/expo/issues/new/choose) in our GitHub repository.
The term "local-first" was first coined in the paper ["Local-first software"](https://www.inkandswitch.com/local-first/), written by the research lab [Ink & Switch](https://www.inkandswitch.com/), but the ideas behind it have been around for a long time. It's the architecture that powers some of our favorite apps, like [Linear](https://linear.app/), [Superhuman](https://superhuman.com/), [Excalidraw](https://excalidraw.com/), and even [Apple Notes](<https://en.wikipedia.org/wiki/Notes_(Apple)>).
In local-first software, "the availability of another computer should never prevent you from working" ([via Martin Kleppmann](https://www.youtube.com/watch?v=NMq0vncHJvU)). When you are offline, you can still read and write directly from/to a database on your device. You can trust the software to work offline, and you know that when you are connected to the internet, your data will be seamlessly synced and available on any of your devices running the app. When you're online, this architecture is well suited for "multiplayer" apps, [as popularized by Figma](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/).
To dig deeper into what local-first is and how it works, refer to the [additional resources](#additional-resources) below.
## Why use local-first architecture?
### User experience benefits
Local-first software feels **fast** because interactions are no longer network-bound, you can read and write directly from/to a database on your device.
You can trust the software to work offline, and you know that when you are connected to the internet, your data will be seamlessly synced and available on any of your devices running the app.
Another characteristic of local-first software is that it is collaborative &mdash; multiple devices can work on the same data, and changes are synced across all of them. This can happen in real-time when you collaborate on a design in [Figma](https://www.figma.com/) or asynchronously when you create a task while offline in Linear and it is synced when you are online again.
### Developer experience benefits
You no longer have to manage various states of your app for each network request &mdash; "loaded", "loading", "error", and so on, with their corresponding UI states and other logic. Write to a local database, and the app will automatically sync the changes to the server. This means that you can focus on building the app, and not worry as much about the networking and offline states.
Your server availability may still be important, but in the event of an outage your users can still access the app and continue working. You may even provide a mechanism to sync the data without going through your server.
## Challenges in building local-first apps
The tools available today are still in their early stages, and so you may find yourself solving problems that you would expect to be solved by the tools you are using today. For example, you may need to implement a custom sync layer, or you may have to figure out how to handle permissions for multiple users operating on the same data. As the ecosystem evolves, we expect it to become easier to build local-first apps. If you're not prepared to be an early adopter, and everything that comes with that, you might want to wait for the tools to mature before you start building your app with local-first tools.
## Tools for building local-first apps
A comprehensive list of tools is available on the ["Local-first software" community website](https://localfirstweb.dev/). The following is a shorter list of tools that we at Expo have had direct experience working with.
One way to think of local-first tools is to group them by the following categories: persistence, state management, and syncing. Some tools will fit into multiple categories if they handle multiple aspects of the problem. Syncing can be further subdivided into syncable data structures and transport layers.
### Legend-State
[Legend-State](https://legendapp.com/open-source/state/v3/) is a super fast all-in-one state and sync library that lets you write less code to make faster apps. It has the following primary goals:
- Faster state management for React apps
- Fine-grained reactivity for minimal renders
- Powerful sync and persistence (with Supabase support built-in)
It works with Expo and React Native (via [`react-native-async-storage`](https://github.com/react-native-async-storage/async-storage?tab=readme-ov-file#react-native-async-storage)). This makes it a perfect match for building local-first mobile and web apps. Get started by using the [Legend-State Supabase example](https://github.com/expo/examples/tree/master/with-legend-state-supabase):
```sh
$ npx create-expo-app --example with-legend-state-supabase
```
### TinyBase
[TinyBase](https://tinybase.org/) calls itself "the reactive data store for local-first apps". It is a state management library that plugs in to many of the most popular syncing and persistence layers, such as [Yjs](#yjs) and [SQLite](#sqlite). It's a great choice for building local-first apps that need to persist and sync data. Get started by using the [TinyBase example](https://github.com/expo/examples/tree/master/with-tinybase):
```sh
$ npx create-expo-app --example with-tinybase
```
TinyBase works seamlessly with Expo Go, allowing you to develop quickly. On Android and iOS, it uses the [`expo-sqlite`](/versions/latest/sdk/sqlite/) library to persist data. On the web, it relies on the [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) API. [Beto Moedano](https://github.com/betomoedano) demonstrates how to build a [Universal Local-first Shopping List App](https://github.com/betomoedano/groceries-shopping-list-app) in the following video:
Video Tutorial: [Watch: Build a Local-First Real-Time Shopping List App with Expo and TinyBase](https://www.youtube.com/watch?v=HqOiB2tDM8Q)
### SQLite
[Expo SQLite](/versions/latest/sdk/sqlite/) is a SQLite library that is a great choice for persistence for local-first apps. You can use SQLite with different state management and syncing layers in front of it, such as [`y-expo-sqlite`](https://github.com/brentvatne/y-expo-sqlite) to persist [Yjs](#yjs) documents, and [TinyBase](#tinybase) as a state management layer. Using SQLite is flexible, but you will need to combine it with other tools or build your own tools to get a complete local-first solution. See [Expo SQLite API reference](/versions/latest/sdk/sqlite/) for more information.
### Yjs
[Yjs](https://github.com/yjs/yjs) is a [CRDT implementation](https://github.com/yjs/yjs?tab=readme-ov-file#yjs-crdt-algorithm) that provides data types that can be synced across multiple clients. When building an app with Yjs and working with data that you would like to be able to sync, then you would use `Y.Array` and `Y.Map` to represent your data rather than `Array` and `Object`. You may use a library like [TinyBase](#tinybase) for state management on top of Yjs, and persistence can be handled by a variety of tools, from a JSON file on your filesystem to a full-fledged database (such as [`y-expo-sqlite`](https://github.com/brentvatne/y-expo-sqlite)) and everything in between. See [Yjs's GitHub repository](https://github.com/yjs/yjs) for more information.
### Prisma
[Prisma](https://prisma.io) is well known as the most popular ORM for Node.js and TypeScript backends, and it's now available for [Expo and React Native in early access](https://www.prisma.io/blog/bringing-prisma-orm-to-react-native-and-expo). Prisma aims to provide a complete local-first solution, with state management, syncing, and persistence all covered for you. While it's still early, [Beto Moedano](https://github.com/betomoedano) has put together a full walkthrough of using Prisma with Expo to build a local-first Notion clone, [check out the code on GitHub](https://github.com/betomoedano/React-Native-Notion-Clone).
Video Tutorial: [Watch: Building a Local-first Notion Clone with React Native Expo and Prisma](https://www.youtube.com/watch?v=uTrPte0sCiw)
### Jazz
[Jazz.tools](https://jazz.tools/docs/react-native) is a framework for building local-first apps. It is open source, provides first-class support for Expo and you can self-host it or use [Jazz Cloud](https://jazz.tools/cloud) to start quickly. [Jazz](https://jazz.tools). To learn more, check out the [examples](https://jazz.tools/examples#react-native) or see the [Getting Started Guide](https://jazz.tools/docs/react-native) for detailed instructions.
### LiveStore
[LiveStore](https://docs.livestore.dev/getting-started/expo/) is a client-centric local-first data layer for high-performance applications. It provides first-class support for Expo, and it's a great choice for building local-first apps. See the blog post on [LiveStore: SQLite-based data layer for local-first apps](https://expo.dev/blog/local-first-application-development-with-livestore).
Video Tutorial: [Watch: How to build local-first native apps with LiveStore and Expo](https://www.youtube.com/watch?v=zQIhJqYU1Qw)
### Turso
[Turso](https://turso.tech) is a modern database service built on SQLite. It now supports [Offline Sync](https://turso.tech/blog/turso-offline-sync-public-beta), which enables true local-first experiences. You can sync databases between local and remote sources with bidirectional sync and built-in conflict detection. While automatic conflict resolution isn't available yet, this feature is still a major step forward. You can use Turso today with [expo-sqlite](/versions/latest/sdk/sqlite). To learn more, read the [Turso: Offline Sync Public Beta](https://turso.tech/blog/turso-offline-sync-public-beta) blog post. For an example integration, check out the [Notes App](https://github.com/betomoedano/notes-app).
Video Tutorial: [Watch: How to build a local-first Notes App with Turso and Expo](https://www.youtube.com/watch?v=SBv32tmyb3k)
### Instant
[Instant](https://www.instantdb.com/) is a modern alternative to Firebase. It gives you a real-time database so you can focus on building your app's frontend. To get started, check out the [Getting Started Guide](https://www.instantdb.com/docs/start-rn). You can also explore the [Sketch App](https://github.com/betomoedano/sketch-app) featured in the video below.
Video Tutorial: [Watch: Build a Local-First Sketch App with Expo, Instant & Reanimated](https://www.youtube.com/watch?v=DEJIcaGN3vY)
### RxDB
[RxDB](https://rxdb.info/) (Reactive Database) is a local-first, NoSQL database for JavaScript applications. It is deeply reactive, allowing you to subscribe to query results so your UI updates automatically when data changes. RxDB focuses on offline-first capabilities to build apps that work even without internet and syncs when back online. RxDB works with Expo using the [SQLite storage adapter](https://rxdb.info/rx-storage-sqlite.html#usage-with-expo-sqlite), which wraps [`expo-sqlite`](/versions/latest/sdk/sqlite/). It also offers a variety of replication plugins to sync with your existing backend, whether it's HTTP, GraphQL, Supabase, or a custom one.
### Other tools
The following list, far from being comprehensive, provides other tools that have caught our attention and that you may find interesting to explore. For a more thorough list of tools, see ["Local-first software" community website](https://localfirstweb.dev/).
- [Automerge](https://automerge.org/)
- [ElectricSQL](https://electric-sql.com/)
- [PowerSync](https://www.powersync.com/)
## Additional resources
- ["The past, present, and future of local-first"](https://www.youtube.com/watch?v=NMq0vncHJvU) by Martin Kleppmann
- ["Local-first software"](https://www.inkandswitch.com/local-first/) by Ink & Switch
- ["Local-first software" community website](https://localfirstweb.dev/) and [meetup playlist on YouTube](https://www.youtube.com/playlist?list=PLTbD2QA-VMnXFsLbuPGz1H-Najv9MD2-H)
- [localfirst.fm podcast](https://localfirst.fm/) by Johannes Schickling


## Keyboard handling

A guide for handling common keyboard interactions on an Android or iOS device.

Keyboard handling is crucial for creating an excellent user experience in your Expo app. React Native provides [`Keyboard`](https://reactnative.dev/docs/keyboard) and [`KeyboardAvoidingView`](https://reactnative.dev/docs/keyboardavoidingview), which are commonly used to handle keyboard events. For more complex or custom keyboard interactions, you can consider using [`react-native-keyboard-controller`](https://kirillzyusko.github.io/react-native-keyboard-controller), which is a library that offers advanced keyboard handling capabilities.
This guide covers common keyboard interactions and how to manage them effectively.
Video Tutorial: [Keyboard Handling tutorial for React Native apps](https://www.youtube.com/watch?v=Y51mDfAhd4E)
## Keyboard handling basics
The following sections explain how to handle keyboard interactions with common APIs.
### Keyboard avoiding view
The `KeyboardAvoidingView` is a component that automatically adjusts a view's height, position, or bottom padding based on the keyboard height to remain visible while it is displayed.
Android and iOS interact with the `behavior` property differently. On iOS, `padding` is usually what works best, and for Android, just having the `KeyboardAvoidingView` prevents covering the input. This is why the following example uses `undefined` for Android. Playing around with the `behavior` is a good practice since a different option could work best for your app.
```tsx HomeScreen.tsx
import { KeyboardAvoidingView, TextInput } from 'react-native';
export default function HomeScreen() {
  return (
    <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
      <TextInput placeholder="Type here..." />
    </KeyboardAvoidingView>;
  );
}
```
In the above example, the height of the `KeyboardAvoidingView` automatically adjusts based on the device's keyboard height, which ensures that the input is always visible.
When using a Bottom Tab navigator on Android, you might notice that focusing on an input field causes the bottom tabs to be pushed above the keyboard. To address this issue, add the `softwareKeyboardLayoutMode` property to your Android configuration in [app config](/workflow/configuration/) and set it to `pan`.
```json app.json
"expo" {
  "android": {
    "softwareKeyboardLayoutMode": "pan"
  }
}
```
After adding this property, restart the development server and reload your app to apply the changes.
It's also possible to hide the bottom tab when the keyboard opens using [`tabBarHideOnKeyboard`](https://reactnavigation.org/docs/bottom-tab-navigator/#tabbarhideonkeyboard). It is an option with the Bottom Tab Navigator. If set to `true`, it will hide the bar when the keyboard opens.
```tsx app/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarHideOnKeyboard: true,
      }}>
      <Tabs.Screen name="index" />
    </Tabs>
  );
}
```
### Keyboard events
The `Keyboard` module from React Native allows you to listen for native events, react to them, and make changes to the keyboard, such as dismissing it.
To listen for keyboard events, use the `Keyboard.addListener` method. This method accepts an event name and a callback function as arguments. When the keyboard is shown or hidden, the callback function is called with the event data.
The following example illustrates a use case for adding a keyboard listener. The state variable `isKeyboardVisible` is toggled each time the keyboard shows or hides. Based on this variable, a button allows the user to dismiss the keyboard only if the keyboard is active. Also, notice that the button uses the `Keyboard.dismiss` method.
```tsx HomeScreen.tsx
import { useEffect, useState } from 'react';
import { Keyboard, View, Button, TextInput } from 'react-native';
export default function HomeScreen() {
  const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
  useEffect(() => {
    const showSubscription = Keyboard.addListener('keyboardDidShow', handleKeyboardShow);
    const hideSubscription = Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
    return () => {
      showSubscription.remove();
      hideSubscription.remove();
    };
  }, []);
  const handleKeyboardShow = event => {
    setIsKeyboardVisible(true);
  };
  const handleKeyboardHide = event => {
    setIsKeyboardVisible(false);
  };
  return (
    <View>
      {isKeyboardVisible && <Button title="Dismiss keyboard" onPress={Keyboard.dismiss} />}
      <TextInput placeholder="Type here..." />
    </View>
  );
}
```
## Advanced keyboard handling with Keyboard Controller
For more complex keyboard interactions, such as larger scrollable entry forms with several text input fields, consider using the [`react-native-keyboard-controller` (Keyboard Controller)](https://kirillzyusko.github.io/react-native-keyboard-controller) library. It offers additional functionality beyond the built-in React Native keyboard APIs, providing consistency across Android and iOS with minimal configuration and offering the native feel users expect.
### Prerequisites
The following steps are described using a [development build](/develop/development-builds/introduction/) since the Keyboard Controller library is not included in Expo Go. See [Create a development build](/develop/development-builds/create-a-build/) for more information.
[Keyboard Controller](https://kirillzyusko.github.io/react-native-keyboard-controller) also requires `react-native-reanimated` to work correctly. To install it, follow these [installation instructions](/versions/latest/sdk/reanimated/#installation).
### Install
Start by installing the Keyboard Controller library in your Expo project:
```sh
$ npx expo install react-native-keyboard-controller
```
### Set up provider
To finalize the setup, add the `KeyboardProvider` to your app.
```tsx app/_layout.tsx
import { Stack } from 'expo-router';
import { KeyboardProvider } from 'react-native-keyboard-controller';
export default function RootLayout() {
  return (
    <KeyboardProvider>
      <Stack>
        <Stack.Screen name="home" />
        <Stack.Screen name="chat" />
      </Stack>
    </KeyboardProvider>
  );
}
```
### Handling multiple inputs
The [`KeyboardAvoidingView`](#keyboard-avoiding-view) component is excellent for prototyping, but it requires platform-specific configuration and is not very customizable.
As a more powerful alternative, you can use the [`KeyboardAwareScrollView`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view) component. It automatically scrolls to a focused `TextInput` and provides native-like performance. For simple screens with only a few elements, using `KeyboardAwareScrollView` is a great approach.
For screens with multiple inputs, the Keyboard Controller library also provides a `KeyboardToolbar` component to use alongside `KeyboardAwareScrollView`. Together, these components handle input navigation and prevent the keyboard from covering the screen without custom configuration:
```tsx FormScreen.tsx
import { TextInput, View, StyleSheet } from 'react-native';
import { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';
export default function FormScreen() {
  return (
    <>
      <KeyboardAwareScrollView bottomOffset={62} contentContainerStyle={styles.container}>
        <View>
          <TextInput placeholder="Type a message..." style={styles.textInput} />
          <TextInput placeholder="Type a message..." style={styles.textInput} />
        </View>
        <TextInput placeholder="Type a message..." style={styles.textInput} />
        <View>
          <TextInput placeholder="Type a message..." style={styles.textInput} />
          <TextInput placeholder="Type a message..." style={styles.textInput} />
          <TextInput placeholder="Type a message..." style={styles.textInput} />
        </View>
        <TextInput placeholder="Type a message..." style={styles.textInput} />
      </KeyboardAwareScrollView>
      <KeyboardToolbar />
    </>
  );
}
const styles = StyleSheet.create({
  container: {
    gap: 16,
    padding: 16,
  },
  listStyle: {
    padding: 16,
    gap: 16,
  },
  textInput: {
    width: 'auto',
    flexGrow: 1,
    flexShrink: 1,
    height: 45,
    borderWidth: 1,
    borderRadius: 8,
    borderColor: '#d8d8d8',
    backgroundColor: '#fff',
    padding: 8,
    marginBottom: 8,
  },
});
```
The above example wraps the inputs with `KeyboardAwareScrollView` to prevent the keyboard from covering them. The `KeyboardToolbar` component displays navigation controls and a dismiss button. While it works without configuration, you can customize the toolbar content if needed.
### Animating views in sync with keyboard height
For a more advanced and customizable approach, you can use [`useKeyboardHandler`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/hooks/keyboard/use-keyboard-handler). It provides access to keyboard lifecycle events. It allows us to determine when the keyboard starts animating and its position in every frame of the animation.
Using the `useKeyboardHandler` hook, you can create a custom hook to access the height of the keyboard at each frame. It uses `useSharedValue` from reanimated to return the height, as shown below.
```tsx ChatScreen.tsx
import { useKeyboardHandler } from 'react-native-keyboard-controller';
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
const useGradualAnimation = () => {
  const height = useSharedValue(0);
  useKeyboardHandler(
    {
      onMove: event => {
        'worklet';
        height.value = Math.max(event.height, 0);
      },
    },
    []
  );
  return { height };
};
```
You can use the `useGradualAnimation` hook to animate a view and give it a smooth animation when the keyboard is active or dismissed, for example, in a chat screen component (shown in the example below). This component gets the keyboard height from the hook. It then creates an animated style called `fakeView` using the `useAnimatedStyle` hook from reanimated. This style only contains one property: `height`, which is set to the keyboard's height.
The `fakeView` animated style is used in an animated view after the `TextInput`. This view's height will animate based on the keyboard's height at each frame, which effectively pushes the content above the keyboard with a smooth animation. It also decreases its height to zero when the keyboard is dismissed.
```tsx ChatScreen.tsx
import { StyleSheet, Platform, FlatList, View, StatusBar, TextInput } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
import { useKeyboardHandler } from 'react-native-keyboard-controller';
import MessageItem from '@/components/MessageItem';
import { messages } from '@/messages';
const useGradualAnimation = () => {
};
export default function ChatScreen() {
  const { height } = useGradualAnimation();
  const fakeView = useAnimatedStyle(() => {
    return {
      height: Math.abs(height.value),
    };
  }, []);
  return (
    <View style={styles.container}>
      <FlatList
        data={messages}
        renderItem={({ item }) => <MessageItem message={item} />}
        keyExtractor={item => item.createdAt.toString()}
        contentContainerStyle={styles.listStyle}
      />
      <TextInput placeholder="Type a message..." style={styles.textInput} />
      <Animated.View style={fakeView} />
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
  },
  listStyle: {
    padding: 16,
    gap: 16,
  },
  textInput: {
    width: '95%',
    height: 45,
    borderWidth: 1,
    borderRadius: 8,
    borderColor: '#d8d8d8',
    backgroundColor: '#fff',
    padding: 8,
    alignSelf: 'center',
    marginBottom: 8,
  },
});
```
## Additional resources


## Building SwiftUI apps with Expo UI

Learn how to use Expo UI to integrate SwiftUI into your Expo apps.

> **info** Available in **SDK 54 and later**.
Expo UI brings SwiftUI to React Native. You can use modern SwiftUI primitives to build your apps.
This guide covers the basics of using Expo UI to integrate SwiftUI into your Expo apps.
Video Tutorial: [Expo UI iOS Liquid Glass Tutorial](https://www.youtube.com/watch?v=2wXYLWz3YEQ)
## Features
- **SwiftUI primitives**: Expo UI is not another UI library. It brings SwiftUI primitives to Expo.
- **1-to-1 mapping**: The components in Expo UI have a 1-to-1 mapping to SwiftUI views. You can easily explore available views in the SwiftUI ecosystem, such as [Explore SwiftUI](https://exploreswiftui.com/) or the [Libraried app](https://apps.apple.com/us/app/libraried-ui-components/id1642862540), and find the corresponding Expo UI component.
- **Full-app support**: Expo UI is designed to be used throughout the entire app. You can write your app entirely in Expo UI, while maintaining flexibility at the same time. The integration works at the component level. You can also mix [React Native components](https://reactnative.dev/docs/components-and-apis), [Expo UI components](/versions/latest/sdk/ui/), [DOM components](/guides/dom-components/), or custom 2D components using [`react-native-skia`](https://shopify.github.io/react-native-skia/).
## Installation
You'll need to install the `@expo/ui` package in your Expo project. Run the following command to install it:
```sh
$ npx expo install @expo/ui
```
## Usage
Expo UI has several SwiftUI components available. You can use them in your app by importing them from `@expo/ui/swift-ui`. However, to cross the boundary from React Native (UIKit) to SwiftUI, you need to use the [`Host`](/versions/latest/sdk/ui/#host) component. The [`Host`](/versions/latest/sdk/ui/#host) is the container for SwiftUI views. You can think of it like [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/svg) in the DOM or [`<Canvas>`](https://shopify.github.io/react-native-skia/docs/canvas/overview/) in [`react-native-skia`](https://shopify.github.io/react-native-skia/). Under the hood, it uses [`UIHostingController`](https://developer.apple.com/documentation/swiftui/uihostingcontroller) to render SwiftUI views in UIKit.
### Basic usage with `Host`
  For Code: 
    ```tsx SwiftUI loading view
    import { CircularProgress, Host } from '@expo/ui/swift-ui';
    import { View, Text } from 'react-native';
    export default function LoadingView() {
      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <Host matchContents>
            <CircularProgress />
          </Host>
          <Text>Loading...</Text>
        </View>
      );
    }
    ```
  For Preview: 
### Using `HStack` and `VStack`
You can also use the `HStack` and `VStack` components to build the entire layout in SwiftUI.
  For Code: 
    ```tsx SwiftUI loading with HStack and VStack
    import { CircularProgress, Host, HStack, LinearProgress, VStack } from '@expo/ui/swift-ui';
    export default function LoadingView() {
      return (
        <Host style={{ flex: 1, margin: 32 }}>
          <VStack spacing={32}>
            <HStack spacing={32}>
              <CircularProgress />
              <CircularProgress color="orange" />
            </HStack>
            <LinearProgress progress={0.5} />
            <LinearProgress color="orange" progress={0.7} />
          </VStack>
        </Host>
      );
    }
    ```
  For Preview: 
### Modifiers
[SwiftUI modifier](<https://developer.apple.com/documentation/swiftui/view/modifier(_:)>) is a powerful way to customize the appearance and behavior of SwiftUI components. Expo UI also provides modifiers for SwiftUI components. You can import modifiers from `@expo/ui/swift-ui/modifiers` and pass them as an array to the `modifiers` prop. In the following example, the [`expo-mesh-gradient`](/versions/latest/sdk/mesh-gradient/) and `glassEffect` modifier are combined to create Liquid Glass text.
  For Code: 
    > **Note**: `glassEffect` modifier requires Xcode 26+ and iOS 26+.
    ```tsx SwiftUI modifiers
    import { Host, Text } from '@expo/ui/swift-ui';
    import { glassEffect, padding } from '@expo/ui/swift-ui/modifiers';
    import { MeshGradientView } from 'expo-mesh-gradient';
    import { View } from 'react-native';
    export default function Page() {
      return (
        <View style={{ flex: 1 }}>
          <MeshGradientView
            style={{ flex: 1 }}
            columns={3}
            rows={3}
            colors={['red', 'purple', 'indigo', 'orange', 'white', 'blue', 'yellow', 'green', 'cyan']}
            points={[
              [0.0, 0.0],
              [0.5, 0.0],
              [1.0, 0.0],
              [0.0, 0.5],
              [0.5, 0.5],
              [1.0, 0.5],
              [0.0, 1.0],
              [0.5, 1.0],
              [1.0, 1.0],
            ]}
          />
          <Host style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0 }}>
            <Text
              size={32}
              modifiers={[
                padding({
                  all: 16,
                }),
                glassEffect({
                  glass: {
                    variant: 'clear',
                  },
                }),
              ]}>
              Glass effect text
            </Text>
          </Host>
        </View>
      );
    }
    ```
  For Preview: 
### iOS Settings app example
Combining the Expo UI components and modifiers, you can build a UI like iOS Settings app.
  For Code: 
    ```tsx SwiftUI Form example to build iOS Settings app
    import {
      Button,
      Form,
      Host,
      HStack,
      Image,
      Section,
      Spacer,
      Toggle,
      Text,
    } from '@expo/ui/swift-ui';
    import { background, clipShape, frame } from '@expo/ui/swift-ui/modifiers';
    import { Link } from 'expo-router';
    import { useState } from 'react';
    export default function SettingsView() {
      const [isAirplaneMode, setIsAirplaneMode] = useState(true);
      return (
        <Host style={{ flex: 1 }}>
          <Form>
            <Section>
              <HStack spacing={8}>
                <Image
                  systemName="airplane"
                  color="white"
                  size={18}
                  modifiers={[
                    frame({ width: 28, height: 28 }),
                    background('#ffa500'),
                    clipShape('roundedRectangle'),
                  ]}
                />
                <Text>Airplane Mode</Text>
                <Spacer />
                <Toggle isOn={isAirplaneMode} onIsOnChange={setIsAirplaneMode} />
              </HStack>
              <Link href="/wifi" asChild>
                <Button>
                  <HStack spacing={8}>
                    <Image
                      systemName="wifi"
                      color="white"
                      size={18}
                      modifiers={[
                        frame({ width: 28, height: 28 }),
                        background('#007aff'),
                        clipShape('roundedRectangle'),
                      ]}
                    />
                    <Text color="primary">Wi-Fi</Text>
                    <Spacer />
                    <Image systemName="chevron.right" size={14} color="secondary" />
                  </HStack>
                </Button>
              </Link>
            </Section>
          </Form>
        </Host>
      );
    }
    ```
  For Preview: 
## Common questions
Note: Can I use flexbox or other styles in SwiftUI components?
---
Flexbox styles can be applied to the `Host` component itself. Once you're inside the SwiftUI context, however, [`Yoga`](https://www.yogalayout.dev/) is not available &mdash; layouts should be defined using `<HStack>` and `<VStack>` instead.
---
Note: What's the  component?
---
`Host` is the container for SwiftUI views. You can think of it like [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/svg) in the DOM or [`<Canvas>`](https://shopify.github.io/react-native-skia/docs/canvas/overview/) in [`react-native-skia`](https://shopify.github.io/react-native-skia/). Under the hood, it uses [`UIHostingController`](https://developer.apple.com/documentation/swiftui/uihostingcontroller) to render SwiftUI views in UIKit.
---
Note: How is Expo UI different from libraries like  or ?
---
Expo UI is not "yet another" UI library and not an opinionated design kit. Instead, it's a primitives library. It exposes native SwiftUI and Jetpack Compose components directly to JavaScript, rather than re-implementing or simulating UI in JavaScript.
---
Note: Can I use  on Android or web?
---
The first milestone for Expo UI is achieving a 1-to-1 mapping from SwiftUI to Expo UI. Universal support will come in the next stage of the roadmap. Our priority is to establish strong SwiftUI support first, and then expand to Jetpack Compose on Android and DOM support on the Web.
---
Note: Can I use React Native components inside SwiftUI components?
---
Yes, you can place React Native components as JSX children of Expo UI components. Expo UI automatically creates a [`UIViewRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewrepresentable) wrapper for you.
However, keep in mind that the SwiftUI layout system works differently from UIKit and has some limitations. According to Apple's documentation:
> **Warning** SwiftUI fully controls the layout of the UIKit view's [`center`](https://developer.apple.com/documentation/UIKit/UIView/center), [`bounds`](https://developer.apple.com/documentation/UIKit/UIView/bounds), [`frame`](https://developer.apple.com/documentation/UIKit/UIView/frame), and [`transform`](https://developer.apple.com/documentation/UIKit/UIView/transform) properties. Don't directly set these layout-related properties on the view managed by a [`UIViewRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewrepresentable) instance from your own code because that conflicts with SwiftUI and results in undefined behavior.
Also note that once you render React Native components, you're leaving the SwiftUI context. If you want to add Expo UI components again, you'll need to reintroduce a `Host` wrapper.
We recommend keeping SwiftUI layouts self-contained. Interop is possible, but it works best when boundaries are clearly defined.
---
Note: I'm a SwiftUI developer. Why should I learn Expo UI?
---
Because React's promise of _"learn once, write anywhere"_, it now extends to SwiftUI and Jetpack Compose. With Expo UI, you can apply your SwiftUI knowledge to build apps that run in the React Native ecosystem, extend to the Web through [DOM components](/guides/dom-components/), and even integrate [2D](https://shopify.github.io/react-native-skia/) and [3D](https://github.com/wcandillon/react-native-webgpu) rendering. The system is flexible enough that different parts of your app can use different approaches &mdash; giving you seamless integration at the component level.
---
## Additional resources