HomeGuidesReferenceLearn

Reference version

ArchiveExpo SnackDiscord and ForumsNewsletter

metro.config.js

A reference of available configurations in Metro.


See more information about metro.config.js in the customizing Metro guide.

Environment variables

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:

.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.

Disabling dotenv files

Dotenv file loading can be fully disabled in Expo CLI by enabling the EXPO_NO_DOTENV environment variable, before invoking any Expo CLI command.

Terminal
# 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

Client environment variables

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.

  • Client environment variables should not contain secrets as they will be viewable in plain-text format in the production binary.
  • Use client environment variables for partially protected values, such as public API keys you don't want to commit to Git or hard-code in your app and that may change depending on the environment.
  • Expo environment variables can be updated while the development server (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.
  • Client environment variable inlining can be disabled with the environment variable EXPO_NO_CLIENT_ENV_VARS=1, this must be defined before any bundling is performed.

CSS

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:

metro.config.js
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:

Terminal
- 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 CSS

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:

styles.css
.container {
  background-color: red;
}

We can then use the class name in our component by importing the stylesheet and using .container:

App.js
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:

index.js
// Applies the styles app-wide.
import 'emoji-mart/css/emoji-mart.css';
  • On native, all global stylesheets are automatically ignored.
  • Hot reloading is supported for global stylesheets, simply save the file and the changes will be applied.

CSS Modules

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:

App.js
// 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 the style prop.

App.js
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>
    </>
  );
}
App.module.css
.text {
  color: red;
}
  • On web, all CSS values are available. CSS is not processed or auto-prefixed like it is with the React Native Web StyleSheet API. You can use postcss.config.js to autoprefix your CSS.
  • CSS Modules use lightningcss under the hood, check the issues for unsupported features.

PostCSS

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:

postcss.config.json
{
  "plugins": {
    "autoprefixer": {}
  }
}

Both postcss.config.json and postcss.config.js are supported, but postcss.config.json enables better caching.

SASS

Expo Metro has partial support for SCSS/SASS.

To setup, install the sass package in your project:

Terminal
- yarn add -D sass

Then, ensure CSS is setup in the metro.config.js file.

  • When sass is installed, then modules without extensions will be resolved in the following order: scss, sass, css.
  • Only use the intended syntax with sass files.
  • Importing other files from inside a scss/sass file is not currently supported.

Tailwind

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:

Terminal
- yarn add -D tailwindcss@^3.3.1

In your app.json ensure the project is using Metro for web:

app.json
{
  "expo": {
    "web": {
      "bundler": "metro"
    }
  }
}

Create global.css in your project:

global.css
/* This file adds the requisite utility classes for Tailwind to work. */
@tailwind base;
@tailwind components;
@tailwind utilities;

Create tailwind.config.js in your project:

tailwind.config.js
/** @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:

metro.config.js
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:

index.js/App.js
// 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]);

Tailwind usage

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>
  );
}

Bare workflow setup

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.

metro.config.js

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

"Bundle React Native code and images"

+			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 absolute | 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 absolute | 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, and ENTRY_FILE environment variables to overwrite these defaults.

Custom entry file

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

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";
}

Production

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 absolute | 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())
}