A reference of available configurations in Metro.
See more information about metro.config.js in the customizing Metro guide.
Environment variables can be loaded using .env files. These files are loaded according to the standard .env file resolution.
If you are migrating an older project to SDK 49 or above, then you should ignore local env files by adding the following to your .gitignore:
# local env files
.env*.local
Dotenv files are Expo CLI-specific as of SDK 49. EAS CLI uses a different mechanism for environment variables until it invokes Expo CLI for compiling and bundling. Learn more about environment variables in EAS.
Dotenv file loading can be fully disabled in Expo CLI by enabling the EXPO_NO_DOTENV
environment variable, before invoking any Expo CLI command.
# All users can run cross-env, followed by the Expo CLI command
-Â
npx cross-env EXPO_NO_DOTENV=1 expo start
# Alternatively, macOS users can define the environment variable, then run npx, followed by the Expo CLI command
-Â
EXPO_NO_DOTENV=1 npx expo start
Environment variables prefixed with EXPO_PUBLIC_
will be exposed to the app at build-time. For example, EXPO_PUBLIC_API_KEY
will be available as process.env.EXPO_PUBLIC_API_KEY
.
Environment variables will not be inlined in code inside of node_modules.
For security purposes, client environment variables are inlined in the bundle, which means that process.env
is not an iterable object, and you cannot dynamically access environment variables. Every variable must be referenced as a static property for it to be inlined. For example, the expression process.env.EXPO_PUBLIC_KEY
will be rewritten and process.env['EXPO_PUBLIC_KEY']
will not.
npx expo start
) is running, without restarting or clearing the bundler cache — however, you'll need to modify and save an included source file to see the updates. You must also perform a client reload, as environment variables do not support Fast Refresh.EXPO_NO_CLIENT_ENV_VARS=1
, this must be defined before any bundling is performed.Available in SDK 49 and higher.
CSS support is under development and currently only works on web.
Expo supports CSS in your project. You can import CSS files from any component. CSS Modules are also supported. To enable CSS, configure your metro.config.js as follows, setting isCSSEnabled
to true
:
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
// Enable CSS support.
isCSSEnabled: true,
});
module.exports = config;
Now you'll need to clear the Metro cache and restart the development server:
-Â
npx expo start --clear
Ensure you don't have a custom Metro transformer that processes CSS files. If you do, you'll need to remove it.
Global styles are web-only, usage will cause your application to diverge visually on native.
You can import a CSS file from any component. The CSS will be applied to the entire page.
Here, we'll define a global style for the class name .container
:
.container {
background-color: red;
}
We can then use the class name in our component by importing the stylesheet and using .container
:
import './styles.css';
import { View } from 'react-native';
export default function App() {
return (
<>
{/* Use `className` to assign the style with React DOM components. */}
<div className="container">Hello World</div>
{/* Use `style` with the following syntax to append class names in React Native for web. */}
<View
style={{
$$css: true,
_: 'container',
}}>
Hello World
</View>
</>
);
}
You can also import stylesheets that are vendored in libraries, just like you would any node module:
// Applies the styles app-wide.
import 'emoji-mart/css/emoji-mart.css';
CSS Modules for native are under development and currently only work on web.
CSS Modules are a way to scope CSS to a specific component. This is useful for avoiding naming collisions and for ensuring that styles are only applied to the intended component.
In Expo, CSS Modules are defined by creating a file with the .module.css
extension. The file can be imported from any component. The exported value is an object with the class names as keys and the web-only scoped names as the values. The import unstable_styles
can be used to access react-native-web
-safe styles.
CSS Modules support platform extensions to allow you to define different styles for different platforms. For example, you can define a module.ios.css
and module.android.css
file to define styles for Android and iOS respectively. You'll need to import without the extension, for example:
// Importing `./App.module.ios.css`:
- import styles from './App.module.css';
+ import styles from './App.module';
Flipping the extension, for example, App.ios.module.css
will not work and result in a universal module named App.ios.module
.
You cannot pass styles to the
className
prop of a React Native or React Native for web component. Instead, you must use thestyle
prop.
import styles, { unstable_styles } from './App.module.css';
export default function Page() {
return (
<>
<Text
style={{
// This is how react-native-web class names are applied
$$css: true,
_: styles.text,
}}>
Hello World
</Text>
<Text style={unstable_styles.text}>Hello World</Text>
{/* Web-only usage: */}
<p className={styles.text}>Hello World</p>
</>
);
}
.text {
color: red;
}
StyleSheet
API. You can use postcss.config.js
to autoprefix your CSS.Changing the Post CSS or
browserslist
config will require you to clear the Metro cache:npx expo start --clear
|npx expo export --clear
.
PostCSS can be customized by adding a postcss.config.json
file to the root of your project. This file should export a function that returns a PostCSS configuration object. For example:
{
"plugins": {
"autoprefixer": {}
}
}
Both postcss.config.json
and postcss.config.js
are supported, but postcss.config.json
enables better caching.
Expo Metro has partial support for SCSS/SASS.
To setup, install the sass
package in your project:
-Â
yarn add -D sass
Then, ensure CSS is setup in the metro.config.js file.
sass
is installed, then modules without extensions will be resolved in the following order: scss
, sass
, css
.sass
files.Tailwind does not support native platforms, this is web-only.
Tailwind can be used with Metro for web. However, due to the advanced caching system in Metro, the setup is a little different from the default Tailwind setup. The following files are modified:
app.json
package.json
global.css
metro.config.js
tailwind.config.js
index.js
To setup, install the tailwindcss
package in your project:
-Â
yarn add -D tailwindcss@^3.3.1
In your app.json ensure the project is using Metro for web:
{
"expo": {
"web": {
"bundler": "metro"
}
}
}
Create global.css in your project:
/* This file adds the requisite utility classes for Tailwind to work. */
@tailwind base;
@tailwind components;
@tailwind utilities;
Create tailwind.config.js in your project:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
// Ensure this points to your source code...
'./app/**/*.{js,tsx,ts,jsx}',
// If you use a `src` folder, add: './src/**/*.{js,tsx,ts,jsx}'
// Do the same with `components`, `hooks`, `styles`, or any other top-level folders...
],
theme: {
extend: {},
},
plugins: [],
};
In metro.config.js, enable CSS support and run the Tailwind CLI:
const path = require('path');
const { getDefaultConfig } = require('expo/metro-config');
const tailwind = require('tailwindcss/lib/cli/build');
module.exports = (async () => {
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
// Enable CSS support.
isCSSEnabled: true,
});
// Run Tailwind CLI to generate CSS files.
await tailwind.build({
'--input': path.relative(__dirname, './global.css'),
'--output': path.resolve(__dirname, 'node_modules/.cache/expo/tailwind/eval.css'),
'--watch': process.env.NODE_ENV === 'development' ? 'always' : false,
'--poll': true,
});
return config;
})();
In your app main entry file, add the following:
// Ensure we import the CSS for Tailwind so it's included in hot module reloads.
const ctx = require.context(
// If this require.context is not inside the root directory (next to the package.json) then adjust this file path
// to resolve correctly.
'./node_modules/.cache/expo/tailwind'
);
if (ctx.keys().length) ctx(ctx.keys()[0]);
You can use Tailwind with React DOM elements as-is:
export default function Page() {
return (
<div className="bg-slate-100 rounded-xl">
<p className="text-lg font-medium">Welcome to Tailwind</p>
</div>
);
}
You can use the { $$css: true }
syntax to use Tailwind with React Native web elements:
import { View, Text } from 'react-native';
export default function Page() {
return (
<View style={{ $$css: true, _: 'bg-slate-100 rounded-xl' }}>
<Text style={{ $$css: true, _: 'text-lg font-medium' }}>Welcome to Tailwind</Text>
</View>
);
}
This guide is versioned and will need to be revisited when upgrading/downgrading Expo. Alternatively, use Expo Prebuild for fully automated setup.
Projects that don't use Expo Prebuild must configure native files to ensure the Expo Metro config is always used to bundle the project.
These modifications are meant to replace npx react-native bundle
and npx react-native start
with npx expo export:embed
and npx expo start
respectively.
Ensure the metro.config.js extends expo/metro-config
:
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
module.exports = config;
android/app/build.gradle
The Android app/build.gradle
must be configured to use Expo CLI for production bundling. Modify the react
config object:
react {
...
+ // Use Expo CLI to bundle the app, this ensures the Metro config
+ // works correctly with Expo projects.
+ cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
+ bundleCommand = "export:embed"
}
ios/<Project>.xcodeproj/project.pbxproj
In your ios/<Project>.xcodeproj/project.pbxproj
file, replace the following scripts:
"Start Packager"
+ shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `$NODE_BINARY --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open `$NODE_BINARY --print \"require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n fi\nfi\n";
Alternatively, in the Xcode project, select the "Start Packager" build phase and add the following modifications:
+ if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
+ source "$PODS_ROOT/../.xcode.env"
+ fi
+ if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
+ source "$PODS_ROOT/../.xcode.env.local"
+ fi
export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > `$NODE_BINARY --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'"`
if [ -z "${RCT_NO_LAUNCH_PACKAGER+xxx}" ] ; then
if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then
echo "Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly"
exit 2
fi
else
- open `$NODE_BINARY --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/launchPackager.command'"` || echo "Can't start packager automatically"
+ open `$NODE_BINARY --print "require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'"` || echo "Can't start packager automatically"
fi
fi
+ shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios relative | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
Alternatively, in the Xcode project, select the "Bundle React Native code and images" build phase and add the following modifications:
if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then
source "$PODS_ROOT/../.xcode.env"
fi
if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then
source "$PODS_ROOT/../.xcode.env.local"
fi
# The project root by default is one level up from the ios directory
export PROJECT_ROOT="$PROJECT_DIR"/..
if [[ "$CONFIGURATION" = *Debug* ]]; then
export SKIP_BUNDLING=1
fi
+ if [[ -z "$ENTRY_FILE" ]]; then
+ # Set the entry JS file using the bundler's entry resolution.
+ export ENTRY_FILE="$("$NODE_BINARY" -e "require('expo/scripts/resolveAppEntry')" "$PROJECT_ROOT" ios relative | tail -n 1)"
+ fi
+ if [[ -z "$CLI_PATH" ]]; then
+ # Use Expo CLI
+ export CLI_PATH="$("$NODE_BINARY" --print "require.resolve('@expo/cli')")"
+ fi
+ if [[ -z "$BUNDLE_COMMAND" ]]; then
+ # Default Expo CLI command for bundling
+ export BUNDLE_COMMAND="export:embed"
+ fi
`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"`
You can set
CLI_PATH
,BUNDLE_COMMAND
, andENTRY_FILE
environment variables to overwrite these defaults.
By default, React Native only supports using a root index.js
file as the entry file (or platform-specific variation like index.ios.js
). Expo projects allow using any entry file, but this requires addition bare setup.
Development mode entry files can be enabled by using the expo-dev-client
package. Alternatively you can add the following configuration:
In the ios/[project]/AppDelegate.mm
file:
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
- return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
+ return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
In the android/app/src/main/java/**/MainApplication.java
:
@Override
protected String getJSMainModuleName() {
- return "index";
+ return ".expo/.virtual-metro-entry";
}
In your ios/<Project>.xcodeproj/project.pbxproj
file, replace the "Bundle React Native code and images"
script to set $ENTRY_FILE
according using Metro:
+ shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios relative | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
The Android app/build.gradle
must be configured to use Metro module resolution to find the root entry file. Modify the react
config object:
+ def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
react {
+ entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
}