# Web

## Develop websites with Expo

Learn how to develop your app for the web so you can build a universal app.

Expo has first-class support for building full-stack websites with React. Expo websites can be [statically rendered](/router/web/static-rendering) for SEO and performance, or client-rendered for a more app-like experience in the browser.
For Universal: 
    Render text on any platform with the `<Text>` component from [React Native for web](https://github.com/necolas/react-native-web).
    ```jsx app/index.js
    import { Text } from 'react-native';
    export default function Page() {
      return <Text>Home page</Text>;
    }
    ```
    React Native for web (RNW) is a set of component libraries such as `<View>`, and `<Text>`, that wrap `react-dom` primitives such as `<div>`, `<p>`, and `<img>`. RNW is optional when developing for web since you can use React DOM directly, but we often recommended it when building across platforms as it maximizes code reuse.
    > React Native for web is used to power the entire [X](https://x.com/) website.
For Web-only: 
    Alternatively, you can write web-only React DOM components such as `<div>`, `<p>`, and so on, however, these components won't render on native platforms.
    ```jsx app/index.js
    export default function Page() {
      return <p>Home page</p>;
    }
    ```
    Building web-only components is fully supported by Expo, however, you may want to organize your code to better support building for both web and native platforms simultaneously. Learn more in [platform-specific modules](/router/advanced/platform-specific-modules).
All of the libraries in the Expo SDK are built to support both browser and server rendering environments (when applicable). Libraries are also optimized for the individual platforms they target.
Development features like Fast Refresh, debugging, environment variables, and [bundling](/guides/customizing-metro) are also fully universal, enabling a unified developer experience. Expo CLI **automatically optimizes code** for individual platforms when you build for production, using techniques like [platform shaking](/guides/tree-shaking#platform-shaking).
## Getting started
### Install web dependencies
```sh
$ npx expo install react-dom react-native-web @expo/metro-runtime
```
Note: Not using the  package in your app yet?
---
If you haven't added Expo to your React Native app yet, you can either [install Expo modules](/bare/installing-expo-modules/) (recommended) or just install the `expo` package and configure the app entry file. This will allow you to target web, but it will not include support for the Expo SDK.
1. Install [Expo CLI](/more/expo-cli/) in your project:
```sh
$ npm install expo
```
2. Modify the entry file to use [`registerRootComponent`](/versions/latest/sdk/expo/#registerrootcomponentcomponent) instead of `AppRegistry.registerComponent`:
```diff
diff --git a/index.js b/index.js
--- a/index.js
+++ b/index.js
@@ -1,5 +1,4 @@
-import {AppRegistry} from 'react-native';
-import {name as appName} from './app.json';
+import {registerRootComponent} from 'expo';
-AppRegistry.registerComponent(appName, () => App);
+registerRootComponent(App);
```
---
### Start the dev server
You can now start the dev server and develop in the browser with:
```sh
$ npx expo start --web
```
The app can be exported as a production website with:
```sh
$ npx expo export --platform web
```
## Next


## Publish websites

Learn how to deploy Expo websites for production.

An Expo web app can be served locally for testing the production behavior, and deployed to a hosting service. We recommend deploying to [EAS Hosting](/eas/hosting) for the best feature support. You can also self-host or use a third-party service.
> For SDK 49 and earlier, you may need the [guide for publishing `webpack` builds](/archive/publishing-websites-webpack/).
## Output targets
The [`web.output`](/versions/latest/config/app/#output) target can be configured in the [app config](/workflow/configuration/) to set the export method for the web app:
```json app.json
{
  "expo": {
    "web": {
      "output": "server",
      "bundler": "metro"
    }
  }
}
```
Expo Router supports three output targets for web apps.
| Output             | Expo Router | API Routes  | Description                                                                                                                                                                      |
| ------------------ | ----------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `single` (default) | <YesIcon /> | <NoIcon />  | Outputs a Single Page Application (SPA) with a single **index.html** in the output directory and has no statically indexable HTML.                                               |
| `server`           | <YesIcon /> | <YesIcon /> | Creates **client** and **server** directories. Client files are output as separate HTML files. API routes as separate JavaScript files for hosting with a custom Node.js server. |
| `static`           | <YesIcon /> | <NoIcon />  | Outputs separate HTML files for every route in the **app** directory.                                                                                                            |
> **info** **Note**: For `static` and `server` output modes, you can configure [global HTTP headers](/router/web/server-headers) that are applied to all route responses via the `expo-router` plugin.
## Create a build
Creating a build of the project is the first step to publishing a web app. Whether you want to serve it locally or deploy to a hosting service, you'll need to export all JavaScript and assets of a project. This is known as a static bundle. It can be exported by running the following command:
Run the universal export command to compile the project for web:
```sh
$ npx expo export -p web
```
The resulting project files are located in the **dist** directory. Any files inside the **public** directory are also copied to the **dist** directory.
## Serve locally
Use `npx expo serve` to quickly test locally how your website will be hosted in production. Run the following command to serve the static bundle:
```sh
$ npx expo serve
```
Open [`http://localhost:8081`](http://localhost:8081) to see your project in action. This is **HTTP only**, so permissions, camera, location, and many other secure features may not work as expected.
## Hosting with EAS
When you're ready to go to production, you can instantly deploy your website with EAS CLI.
## Hosting on third-party services
### Netlify
[Netlify](https://www.netlify.com/) is a mostly-unopinionated platform for deploying web apps. This has the highest compatibility with Expo web apps as it makes few assumptions about the framework.
#### Manual deployment with the Netlify CDN
Step 1: 
Install the Netlify CLI by running the following command:
```sh
$ npm install -g netlify-cli
```
Step 2: 
Configure redirects for single-page applications.
> If your app uses [static rendering](/router/web/static-rendering), then you can skip this step.
`expo.web.output: 'single'` generates a single-page application. It means there's only one **dist/index.html** file to which all requests must be redirected. This can be done in Netlify by creating a **./public/\_redirects** file and redirecting all requests to **/index.html**.
```sh public/_redirects
/*    /index.html   200
```
If you modify this file, you must rebuild your project with `npx expo export -p web` to have it safely copied into the **dist** directory.
Step 3: 
Deploy the web build directory by running the following command:
```sh
$ netlify deploy --dir dist
```
You'll see a URL that you can use to view your project online.
#### Continuous delivery
Netlify can also build and deploy when you push to git or open a new pull request:
- [Start a new Netlify project](https://app.netlify.com/signup).
- Pick your Git hosting service and select your repository.
- Click **Build your site**.
### Vercel
[Vercel](https://vercel.com/) has a single-command deployment flow.
Step 1: 
Install the [Vercel CLI](https://vercel.com/docs/cli).
```sh
$ npm install -g vercel@latest
```
Step 2: 
  Configure redirects for single-page applications.
Create a **vercel.json** file at the root of your app and add the following configuration:
```json vercel.json
{
  "buildCommand": "expo export -p web",
  "outputDirectory": "dist",
  "devCommand": "expo",
  "cleanUrls": true,
  "framework": null,
  "rewrites": [
    {
      "source": "/:path*",
      "destination": "/"
    }
  ]
}
```
If your app uses [static rendering](/router/web/static-rendering), then you may want to add additional [dynamic route configuration](/router/web/static-rendering#dynamic-routes).
Step 3: 
  Deploy the website.
{' '}
```sh
$ vercel
```
You'll now see a URL that you can use to view your project online. Paste that URL into your browser when the build is complete, and you'll see your deployed app.
### AWS Amplify Console
The [AWS Amplify Console](https://console.amplify.aws) provides a Git-based workflow for continuously deploying and hosting full-stack serverless web apps. Amplify deploys your PWA from a repository instead of from your computer. In this guide, we'll use a GitHub repository. Before starting, [create a new repo on GitHub](https://github.com/new).
Step 1: 
Add the [**amplify-explicit.yml**](https://github.com/expo/amplify-demo/blob/master/amplify-explicit.yml) file to the root of your repository. Ensure you have removed the generated **dist** directory from the **.gitignore** file and committed those changes.
Step 2: 
Push your local Expo project to a GitHub repository. If you haven't pushed to GitHub yet, follow [GitHub's guide to add an existing project to GitHub](https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/adding-locally-hosted-code-to-github).
Step 3: 
Login to the [Amplify Console](https://console.aws.amazon.com/amplify/home) and select an existing app or create a new app. Grant Amplify permission to read from your GitHub account or the organization that owns your repo.
Step 4: 
Add your repo, select the branch, and select **Connecting a monorepo?** to enter the path to your app's **dist** directory and choose **Next**.
The Amplify Console will detect the **amplify.yml** file in your project. Select **Allow AWS Amplify to automatically deploy all files hosted in your project root directory** and choose **Next**.
Step 5: 
Review your settings and choose **Save and deploy**. Your app will now be deployed to a `https://branchname.xxxxxx.amplifyapp.com` URL. You can now visit your web app, deploy another branch, or add a unified backend environment across your Expo mobile and web apps.
Follow the steps in the **Learn how to get the most out of Amplify Hosting** drop-down to **Add a custom domain with a free SSL certificate** and more information.
### Firebase hosting
[Firebase Hosting](https://console.firebase.google.com/) is production-grade web content hosting for web projects.
Step 1: 
Create a firebase project with the [Firebase Console](https://console.firebase.google.com) and install the Firebase CLI by following these [instructions](https://firebase.google.com/docs/hosting).
Step 2: 
Using the CLI, login to your Firebase account by running the command:
```sh
$ firebase login
```
Step 3: 
Then, initialize your firebase project to host by running the command:
```sh
$ firebase init
```
The settings will depend on how you built your Expo website:
1. When asked about the public path, make sure to specify the **dist** directory.
2. When prompted **Configure as a single-page app (rewrite all urls to /index.html)**, only select **Yes** if you used `web.output: "single"` (default). Otherwise, select **No**.
Step 4: 
In the existing `scripts` property of **package.json**, add `predeploy` and `deploy` properties. Each has the following values:
```json package.json
"scripts": {
  "predeploy": "expo export -p web",
  "deploy-hosting": "npm run predeploy && firebase deploy --only hosting",
}
```
Step 5: 
To deploy, run the following command:
```sh
$ npm run deploy-hosting
```
Open the URL from the console output to check your deployment, for example: `https://project-name.firebaseapp.com`.
In case you want to change the header for hosting add the following config for `hosting` section in **firebase.json**:
```json firebase.json
  "hosting": [
    {
      "headers": [
        {
          "source": "/**",
          "headers": [
            {
              "key": "Cache-Control",
              "value": "no-cache, no-store, must-revalidate"
            }
          ]
        },
        {
          "source": "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|woff2|font.css)",
          "headers": [
            {
              "key": "Cache-Control",
              "value": "max-age=604800"
            }
          ]
        }
      ],
    }
  ]
```
### GitHub Pages
[GitHub Pages](https://pages.github.com/) allows you to publish a website directly from a GitHub repository.
> **warning** GitHub Pages deployment uses experimental `baseUrl` functionality that may not work as intended.
Step 1: 
Start by initializing a new git repository in your project and configuring it to push to a GitHub repository. If you are already syncing your changes with a GitHub repository, skip this step.
Create a repository on the GitHub website. Then, run the following commands in your project's root directory:
```sh
$ git init
$ git remote add origin https://github.com/username/expo-gh-pages.git
```
The above commands initialize a new Git repository and configure it to push your source code to the specified GitHub repository.
Step 2: 
Install the `gh-pages` package as a development dependency in your project:
<Terminal
  cmd={{
    npm: ['$ npm install --save-dev gh-pages'],
    yarn: ['$ yarn add -D gh-pages'],
    pnpm: ['$ pnpm add -D gh-pages'],
    bun: ['$ bun add -D gh-pages'],
  }}
/>
Step 3: 
To deploy the project, configure it to a subdomain with the [`baseUrl`](/versions/latest/config/app/#baseurl) property in [app config](/workflow/configuration/). Set its value to the string `/repo-name`.
For example, if the GitHub repository is `expo-gh-pages`, the following will be the value of the [experimental `baseUrl` property](/more/expo-cli/#hosting-with-sub-paths):
```json app.json
{
  "expo": {
    "experiments": {
      "baseUrl": "/expo-gh-pages"
    }
  }
}
```
Step 4: 
Modify the `scripts` in the **package.json** file by adding `predeploy` and `deploy` scripts. Each has its own value:
```json package.json
"scripts": {
  "deploy": "gh-pages --nojekyll -d dist",
  "predeploy": "expo export -p web"
}
```
Since Expo uses underscores in generated files, you need to disable Jekyll with the `--nojekyll` flag.
Step 5: 
To generate a production build of the web app and deploy it to GitHub Pages, run the following command:
<Terminal
  cmd={{
    npm: ['$ npm run deploy'],
    yarn: ['$ yarn run deploy'],
    pnpm: ['$ pnpm run deploy'],
    bun: ['$ bun run deploy'],
  }}
/>
This publishes a build of the web app to the `gh-pages` branch of your GitHub repository. This branch only contains build artifacts from the **dist** directory, plus the **.nojekyll** file generated by `gh-pages`. It does not include development source code.
Step 6: 
Now that the web app is published to the `gh-pages` branch, configure GitHub Pages to serve the app from that branch.
- Navigate to the **Settings** tab of the GitHub repository.
- Scroll down to **Pages** section.
- Ensure the **Source** is set to **Deploy from a branch**.
- Under **Branch** section, select **gh-pages** and the **root** directory.
- Click **Save**.
Step 7: 
Once the web app is published and the GitHub Pages configuration is set, a GitHub Action will deploy your website. You can monitor its progress by navigating to your repository's **Actions** tab. Upon completion, your web app will be available at the URL `http://username-on-github.github.io/repo-name`.
For subsequent deployments and updates, run the `deploy` command and the GitHub Action will start automatically to update your web app.


## Using React DOM in Expo native apps

Learn about rendering React DOM components in Expo native apps using the 'use dom' directive.

> **info** Available in **SDK 52 and later**.
Expo offers a novel approach to work with modern web code directly in a native app via the `'use dom'` directive. This enables incremental migration for an entire website to a universal app by moving on a per-component basis.
While the Expo native runtime generally does not support elements like `<div>` or `<img>`, there may be instances where you need to quickly incorporate web components. In such cases, DOM components provide a useful solution.
## Prerequisites
Note: Your project must use Expo CLI and extend the Expo Metro Config
---
If you already run your project with `npx expo [command]` (for example, if you created it with `npx create-expo-app`), then you're all set, and you can skip this step.
If you don't have the `expo` package in your project yet, then install it by running the command below and [opt in to using Expo CLI and Metro Config](/bare/installing-expo-modules/#configure-expo-cli-for-bundling-on-android-and-ios):
```sh
$ npx install-expo-modules@latest
```
If the command fails, refer to the [Installing Expo modules](/bare/installing-expo-modules/#manual-installation) guide.
---
Note: Expo Metro Runtime, React DOM, and React Native Web
---
If you are using Expo Router and Expo Web, you can skip this step. Otherwise, install the following packages:
```sh
$ npx expo install @expo/metro-runtime react-dom react-native-web
```
---
## Usage
Install `react-native-webview` in your project:
```sh
$ npx expo install react-native-webview
```
To render a React component to the DOM, add the `'use dom'` directive to the top of the web component file:
```tsx my-component.tsx (web)
'use dom';
export default function DOMComponent({ name }: { name: string }) {
  return (
    <div>
      <h1>Hello, {name}</h1>
    </div>
  );
}
```
Inside the native component file, import the web component to use it:
```tsx App.tsx (native)
import DOMComponent from './my-component.tsx';
export default function App() {
  return (
    // This is a DOM component. It re-exports a wrapped `react-native-webview` behind the scenes.
    <DOMComponent name="Europa" />
  );
}
```
## Features
- Shared bundler config across web, native, and DOM components.
- React, TypeScript, CSS, and all other Metro features are enabled in DOM components.
- Logging in the terminal and Safari/Chrome debugging.
- Fast Refresh and HMR.
- Embedded exports for offline support.
- Assets are unified across web and native.
- DOM component bundles can be introspected in [Expo Atlas](/guides/analyzing-bundles/#analyzing-bundle-size-with-atlas) for debugging.
- Access to all web functionality without needing a native rebuild.
- Runtime error overlay in development.
- Supports Expo Go.
## WebView props
To pass props to the underlying native **WebView**, use the `dom` prop on the component. This prop is built into every DOM component and accepts an object with any [`WebView` props](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md) that you would like to change.
```tsx App.tsx (native)
import DOMComponent from './my-component';
export default function App() {
  return (
    <DOMComponent
      dom={{
        scrollEnabled: false,
      }}
    />
  );
}
```
On your DOM component, add the `dom` prop so it is recognized in TypeScript:
```tsx my-component.tsx (web)
'use dom';
export default function DOMComponent({}: { dom: import('expo/dom').DOMProps }) {
  return (
    <div>
      <h1>Hello, world!</h1>
    </div>
  );
}
```
## Marshalled props
You can send data to the DOM component through serializable props (`number`, `string`, `boolean`, `null`, `undefined`, `Array`, `Object`). For example, inside a native component file, you can pass a prop to the DOM component:
```tsx App.tsx (native)
import DOMComponent from './my-component';
export default function App() {
  return <DOMComponent hello={'world'} />;
}
```
Inside the web component file, you can receive the prop as shown in the example below:
```tsx my-component.tsx (web)
'use dom';
export default function DOMComponent({ hello }: { hello: string }) {
  return <p>Hello, {hello}</p>;
}
```
Props are sent over an asynchronous bridge so they are not updated synchronously. They are passed as props to the React root component, which means they re-render the entire React tree.
## Native actions
You can send type-safe native functions to DOM components by passing asynchronous functions as top-level props to the DOM component:
```tsx App.tsx (native)
import DomComponent from './my-component';
export default function App() {
  return (
    <DomComponent
      hello={(data: string) => {
        console.log('Hello', data);
      }}
    />
  );
}
```
```tsx my-component.tsx (web)
'use dom';
export default function MyComponent({ hello }: { hello: (data: string) => Promise<void> }) {
  return <p onClick={() => hello('world')}>Click me</p>;
}
```
> You cannot pass functions as nested props to DOM components. They must be top-level props.
Native actions are always asynchronous and accept only serializable arguments (meaning no functions) because the data is sent over a bridge to the DOM component's JavaScript engine.
Native actions can return serializable data to the DOM component, which is useful for getting data back from the native side.
```tsx
getDeviceName(): Promise<string> {
  return DeviceInfo.getDeviceName();
}
```
Think of these functions like React Server Functions, but instead of residing on the server, they live locally in the native app and communicate with the DOM component. This approach provides a powerful way to add truly native functionality to your DOM components.
## Passing refs
> **important** This is in alpha and may change in the future.
You can use the `useDOMImperativeHandle` hook inside a DOM component to accept ref calls from the native side. This hook is similar to React's [`useImperativeHandle`](https://react.dev/reference/react/useImperativeHandle) hook, but it does not need a ref object to be passed to it.
```tsx App.tsx (native)
import { useRef } from 'react';
import { Button, View } from 'react-native';
import MyComponent, { type DOMRef } from './my-component';
export default function App() {
  const ref = useRef<DOMRef>(null);
  return (
    <View style={{ flex: 1 }}>
      <MyComponent ref={ref} />
      <Button
        title="focus"
        onPress={() => {
          ref.current?.focus();
        }}
      />
    </View>
  );
}
```
For SDK 53 and later: 
Expo SDK 53 and later use React 19. This means that the `ref` prop is passed to the component as a prop, and you can use it directly in the component.
```tsx my-component.tsx (web)
'use dom';
import { useDOMImperativeHandle, type DOMImperativeFactory } from 'expo/dom';
import { Ref, useRef } from 'react';
export interface DOMRef extends DOMImperativeFactory {
  focus: () => void;
}
export default function MyComponent(props: {
  ref: Ref<DOMRef>;
  dom?: import('expo/dom').DOMProps;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  useDOMImperativeHandle(
    props.ref,
    () => ({
      focus: () => {
        inputRef.current?.focus();
      },
    }),
    []
  );
  return <input ref={inputRef} />;
}
```
For SDK 52 and earlier: 
In Expo SDK 52 and earlier (React 18), use the legacy `forwardRef` function to access the `ref` handle.
```tsx my-component.tsx (web)
'use dom';
import { useDOMImperativeHandle, type DOMImperativeFactory } from 'expo/dom';
import { forwardRef, useRef } from 'react';
export interface MyRef extends DOMImperativeFactory {
  focus: () => void;
}
export default forwardRef<MyRef, object>(function MyComponent(props, ref) {
  const inputRef = useRef<HTMLInputElement>(null);
  useDOMImperativeHandle(
    ref,
    () => ({
      focus: () => {
        inputRef.current?.focus();
      },
    }),
    []
  );
  return <input ref={inputRef} />;
});
```
React is meant to have a unilateral data flow, so the concept of using callbacks to go back up the tree is not idiomatic. Expect the behavior to be flakey and possibly phased out in the future with newer versions of React. The preferred way to send data back up the tree is to use native actions, which update the state and then pass it back to the DOM component.
## Feature detection
Since DOM components are used to run websites, you might need extra qualifiers to better support certain libraries. You can detect if a component is running in a DOM component with the following code:
```ts
import { IS_DOM } from 'expo/dom';
```
While `process.env.EXPO_OS` will always be web in a DOM component, you can detect the _top_ platform with `process.env.EXPO_DOM_HOST_OS`. This will be either `ios`, `android`, depending on the OS of the top-most native platform, and `undefined` on web.
## Public assets
> **important** **Warning:** This is in alpha and may change in the future. Public assets are not supported in EAS Update. Use `require()` to load local assets instead.
The contents of the root **public** directory are copied to the native app's binary to support the use of public assets in DOM components. Since these public assets will be served from the local filesystem, use the `process.env.EXPO_BASE_URL` prefix to reference the correct path. For example:
```tsx
<img src={`${process.env.EXPO_BASE_URL}img.png`} />
```
## Debugging
By default, all `console.log` methods are extended in WebViews to forward logs to the terminal. This makes it fast and easy to see what's happening in your DOM components.
Expo also enables WebView inspection and debugging when bundling in development mode. You can open **Safari** > **Develop** > **Simulator** > **MyComponent.tsx** to see the WebView's console and inspect elements.
## Manual WebViews
You can create a manual WebView using the `WebView` component from `react-native-webview`. This can be useful for rendering websites from a remote server.
```tsx App.tsx (native)
import { WebView } from 'react-native-webview';
export default function App() {
  return <WebView source={{ html: '<h1>Hello, world!</h1>' }} />;
}
```
## Routing
Expo Router APIs such as `<Link />`, and `useRouter` can be used in DOM components to navigate between routes.
```tsx my-component.tsx (web)
'use dom';
import Link from 'expo-router/link';
export default function DOMComponent() {
  return (
    <div>
      <h1>Hello, world!</h1>
      <Link href="/about">About</Link>
    </div>
  );
}
```
APIs that synchronously return routing info such as `useLocalSearchParams()`, `useGlobalSearchParams()`, `usePathname()`, `useSegments()`, `useRootNavigation()`, and `useRootNavigationState()` are not automatically supported. Instead, read these values outside of DOM components and supply them as props.
```tsx App.tsx (native)
import DOMComponent from './my-component';
import { usePathname } from 'expo-router';
export default function App() {
  const pathname = usePathname();
  return <DOMComponent pathname={pathname} />;
}
```
The `router.canGoBack()` and `router.canDismiss()` functions are also unsupported and require manual marshalling, this ensures no extraneous render cycles are triggered.
Avoid using standard web `<a />` anchor elements for navigation as these will change the DOM component origin in a way that users may not be able to navigate back from. Prefer launching `WebBrowser`s if you want to present external websites.
Since DOM components cannot render native children, layout routes (`_layout`) can never be DOM components. You can render DOM components from layout routes to create headers, backgrounds, and more, but the layout route itself should always be native.
## Measuring DOM components
You may want to measure the size of a DOM component and report it back to the native side (for example, native scrolling). This can be done using a `matchContents` prop or a manual native action:
### Automatically with `matchContents` prop
You can use the `dom={{ matchContents: true }}` prop to measure the size of the DOM component automatically and resize the native view. This is particularly useful for certain layouts where the DOM component must have an intrinsic size in order to be displayed, such as when the component is centered within a parent view:
```tsx App.tsx (native)
import DOMComponent from './my-component';
export default function Route() {
  return <DOMComponent dom={{ matchContents: true }} />;
}
```
### Manually by specifying a size
You can also manually provide a size by passing it to the `WebView` `style` prop via the `dom` prop:
```tsx App.tsx (native)
import DOMComponent from './my-component';
export default function Route() {
  return (
    <DOMComponent
      dom={{
        style: { width, height },
      }}
    />
  );
}
```
### Observing changes in size
If you would like to report changes in the size of the DOM component back to the native side, you can add a native action to your DOM component that is called whenever the size is changed:
```tsx my-component.tsx (web)
'use dom';
import { useEffect } from 'react';
function useSize(callback: (size: { width: number; height: number }) => void) {
  useEffect(() => {
    // Observe window size changes
    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        callback({ width, height });
      }
    });
    observer.observe(document.body);
    callback({
      width: document.body.clientWidth,
      height: document.body.clientHeight,
    });
    return () => {
      observer.disconnect();
    };
  }, [callback]);
}
export default function DOMComponent({
  onDOMLayout,
}: {
  dom?: import('expo/dom').DOMProps;
  onDOMLayout: (size: { width: number; height: number }) => void;
}) {
  useSize(onDOMLayout);
  return <div style={{ width: 500, height: 500, background: 'blue' }} />;
}
```
Then update your native code to set the size in state whenever the DOM component reports a change in size:
```tsx App.tsx (native)
import DOMComponent from '@/components/my-component';
import { useState } from 'react';
import { View, ScrollView } from 'react-native';
export default function App() {
  const [containerSize, setContainerSize] = useState<{
    width: number;
    height: number;
  } | null>(null);
  return (
    <View style={{ flex: 1 }}>
      <ScrollView>
        <DOMComponent
          onDOMLayout={async ({ width, height }) => {
            if (containerSize?.width !== width || containerSize?.height !== height) {
              setContainerSize({ width, height });
            }
          }}
          dom={{
            containerStyle:
              containerSize != null
                ? { width: containerSize.width, height: containerSize.height }
                : null,
          }}
        />
      </ScrollView>
    </View>
  );
}
```
## Architecture
Built-in DOM support only renders websites as single-page applications (no SSR or SSG). This is because search engine optimization and indexing are unnecessary for embedded JS code.
When a module is marked with `'use dom'`, it is replaced with a proxy reference imported at runtime. This feature is primarily achieved through a series of bundler and CLI techniques.
If desired, you can still use a WebView with the standard approach by passing raw HTML to a `WebView` component.
DOM components rendered within websites or other DOM components will behave as regular components, and the `dom` prop will be ignored. This is because web content is passed directly through and not wrapped in an `iframe`.
Overall, this system shares many similarities with Expo's React Server Components implementation.
## Considerations
We recommend building truly native apps using universal primitives such as `View`, `Image`, and `Text`. DOM components only support standard JavaScript, which is slower to parse and start up than optimized Hermes bytecode.
Data can be sent between DOM components and native components only through an asynchronous JSON transport system. Avoid relying on data across JS engines and deep linking to nested URLs in DOM components, as they do not currently support full reconciliation with Expo Router.
While DOM components are not exclusive to Expo Router, they are developed and tested against Expo Router apps to provide the best experience when used with Expo Router.
If you have a global state for sharing data, it will not be accessible across JS engines.
While native modules in the Expo SDK can be optimized to support DOM components, this optimization has not been implemented yet. Use native actions and props to share native functionality with DOM components.
DOM components and websites in general are less optimal than native views but there are some reasonable uses for them. For example, the web is conceptually the best way to render rich-text and markdown. The web also has very good WebGL support, with the caveat that devices in low-power mode will often throttle web frame rates to preserve battery.
Many large apps also use some web content for auxiliary routes such as blog posts, rich-text (for example, long-form posts on X), settings pages, help pages, and other less frequently visited parts of the app.
## Server Components
DOM components currently only render as single-page applications and don't support static rendering or React Server Components (RSC). When the project uses React Server Components, `'use dom'` will work the same as `'use client'` regardless of the platform. RSC Payloads can be passed as properties to DOM components. However, they cannot be hydrated correctly on native platforms as they'll be rendered for a native runtime.
## Limitations
- Unlike server components, you cannot pass `children` to DOM components.
- DOM components are standalone and do not automatically share data between different instances.
- You cannot add native views to DOM components. While you can attempt to float native views over DOM components, this approach results in a suboptimal user experience.
- Function props cannot return values synchronously. They must be asynchronous.
- DOM components can currently only be embedded and do not support OTA updates. This functionality may be added in the future as part of React Server Components.
Ultimately, universal architecture is the most exciting kind. Expo CLI's extensive universal tooling is the only reason we can even offer a feature as intricate and valuable as this one.
While DOM components help with migration and moving quickly, we recommend using truly native views whenever possible.
## Common questions
### How to obtain a Secure Context in DOM components?
Some Web APIs require a [Secure Context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) function correctly. For example, the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) is only available in secure contexts. A secure context means that remote resources must be served over HTTPS. [Learn more about features restricted to secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts).
To ensure your DOM components run within a secure context, follow these guidelines:
- **Release builds**: DOM components served using the `file://` scheme are provided a secure context by default.
- **Debug builds**: When using development servers (which default to the `http:// `protocol), you can use [tunneling](/more/expo-cli/#tunneling) to serve DOM components over HTTPS.
Note: Example commands to tunnel DOM components over HTTPS
---
```sh
$ npx expo install expo-dev-client
$ npx expo run:android
$ npx expo start --tunnel -d -a
$ npx expo run:ios
$ npx expo start --tunnel -d -i
```
---


## Progressive web apps

Learn how to add progressive web app support to Expo websites.

A progressive web app (or PWA for short) is a website that can be installed on the user's device and used offline. We recommend building native apps whenever possible as they have the best offline support, but PWAs are a great option for desktop users.
## Favicons
Expo CLI automatically generates the **favicon.ico** file based on the `web.favicon` field in the **app.json**.
```json
{
  "web": {
    "favicon": "./assets/favicon.png"
  }
}
```
Alternatively, you can create a **favicon.ico** file in the **public** directory to manually specify the icon.
## Manifest file
PWAs can be [configured with a manifest file](https://developer.mozilla.org/en-US/docs/Web/Manifest) that describes the app's name, icon, and other metadata.
Step 1: 
Create a PWA manifest in **public/manifest.json**:
```json
{
  "short_name": "Expo App",
  "name": "Expo Router Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}
```
Step 2: 
The files **logo192.png** and **logo512.png** are the icons that will be used when the app is installed on the user's device. These should be added to the **public** directory too.
```
âââ public/
    âââ manifest.json  # PWA Manifest
    âââ logo192.png  # 192x192 icon
    âââ logo512.png  # 512x512 icon
```
Step 3: 
Now link the manifest in your HTML file. The method here depends on the output mode of your website (indicated in `web.output` in the **app.json**ââdefaults to `single`).
For single: 
If you're using a single-page app, you can link the manifest in your HTML file by first creating a template HTML in **public/index.html**:
```sh
$ npx expo customize public/index.html
```
Then add the manifest to the `<head>` tag:
```html
<link rel="manifest" href="/manifest.json" />
```
For static & server: 
If you're using static or server rendering, the HTML entry can be dynamically created in **app/+html.tsx**. Here we'll link the manifest by adding a `<link>` tag to the `<head>` component:
```tsx app/+html.tsx
import { ScrollViewStyleReset } from 'expo-router/html';
import type { PropsWithChildren } from 'react';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        {/* Link the PWA manifest file. */}
        <link rel="manifest" href="/manifest.json" />
        {/*
          Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
          However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
        */}
        <ScrollViewStyleReset />
        {/* Add any additional <head> elements that you want globally available on web... */}
      </head>
      <body>{children}</body>
    </html>
  );
}
```
## Service workers
Service workers are primarily used to add offline support to websites. Google's Workbox is the best way to add service workers to a website. Follow the guide for [using Workbox CLI](https://developer.chrome.com/docs/workbox/modules/workbox-cli/), and wherever it refers to a "build script" use `npx expo export -p web` instead.
> **warning** Be careful adding service workers as they are known to cause unexpected behavior on web. If you accidentally ship a service worker that aggressively caches your website, users cannot request updates easily. For the best offline mobile experience, create a native app with Expo. Unlike websites with service workers, native apps can be updated through the app store to clear the cached experience. This would be similar to resetting the user's native browser (which they may have to do if the service worker is aggressive enough). See [why service workers are suboptimal](https://github.com/facebook/create-react-app/issues/2398) for more information.
For example, here's a possible flow for setting up Workbox:
Step 1: 
Create a new project with the following command:
```sh
$ npm create expo -t tabs my-app
$ cd my-app
```
Step 2: 
Now register the service worker in the HTML file. The method here depends on the output mode of your website (indicated in `web.output` in the **app.json**ââdefaults to `single`).
For single: 
Next add a service worker registration script to the root **index.html**.
First create a template HTML in **public/index.html** if one does not already exist:
```sh
$ npx expo customize public/index.html
```
Then create the service worker registration script in the `<head>` tag:
```html
<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker
        .register('/sw.js')
        .then(registration => {
          console.log('Service Worker registered with scope:', registration.scope);
        })
        .catch(error => {
          console.error('Service Worker registration failed:', error);
        });
    });
  }
</script>
```
For static & server: 
Next, create a root HTML file for the app and add the service worker registration script:
```tsx app/+html.tsx
import { ScrollViewStyleReset } from 'expo-router/html';
import type { PropsWithChildren } from 'react';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        {/* Bootstrap the service worker. */}
        <script dangerouslySetInnerHTML={{ __html: sw }} />
        {/*
          Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
          However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
        */}
        <ScrollViewStyleReset />
        {/* Add any additional <head> elements that you want globally available on web... */}
      </head>
      <body>{children}</body>
    </html>
  );
}
const sw = `
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js').then(registration => {
            console.log('Service Worker registered with scope:', registration.scope);
        }).catch(error => {
            console.error('Service Worker registration failed:', error);
        });
    });
}
`;
```
Step 3: 
Now build the website before running the wizard:
```sh
$ npx expo export -p web
```
Step 4: 
Run the wizard command, select `dist` as the root of the app, and the defaults for everything else:
```sh
$ npx workbox-cli wizard
? What is the root of your web app (that is which directory do you deploy)? dist/
? Which file types would you like to precache? js
html
ttf
ico
json
? Where would you like your service worker file to be saved? dist/sw.js
? Where would you like to save these configuration options? workbox-config.js
? Does your web app manifest include search parameter(s) in the 'start_url
other than 'utm_' or 'fbclid' (like '?source=pwa')? No
```
Step 5: 
Finally, run `npx workbox-cli generateSW workbox-config.js` to generate the service worker config.
Going forward, you can add a build script in **package.json** to run both scripts in the correct order:
```json package.json
{
  "scripts": {
    "build:web": "expo export -p web && npx workbox-cli generateSW workbox-config.js"
  }
}
```
Step 6: 
If you host your website and visit with Chrome, you can inspect the service worker by going to **Application > Service Workers** in the Chrome DevTools.


## Tailwind CSS

Learn how to configure and use Tailwind CSS in your Expo project.

> **info** Standard Tailwind CSS supports only web platform. For universal support, use a library such as [NativeWind](https://www.nativewind.dev/), which allows creating styled React Native components with Tailwind CSS.
[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework and can be used with Metro for web projects. This guide explains how to configure your Expo project to use the framework.
## Prerequisites
The following files will be modified to set the Tailwind CSS configuration:
```
âââ app.json
âââ package.json
âââ global.css
âââ index.js
```
Ensure that your project is using Metro for web. You can verify this by checking the `web.bundler` field which is set to `metro` in the **app.json** file.
```json app.json
{
  "expo": {
    "web": {
      "bundler": "metro"
    }
  }
}
```
## Configuration
Configure Tailwind CSS in your Expo project according to the [Tailwind PostCSS documentation](https://tailwindcss.com/docs/installation/using-postcss).
For v3: 
Step 1: 
Install `tailwindcss` and its required peer dependencies. Then, the run initialization command to create **tailwind.config.js** and **post.config.js** files in the root of your project.
```sh
$ npx expo install tailwindcss@3 postcss autoprefixer --dev
$ npx tailwindcss init -p
```
Step 2: 
Add paths to all of your template files inside **tailwind.config.js**.
```js 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` directory, add: './src/**/*.{js,tsx,ts,jsx}'
    // Do the same with `components`, `hooks`, `styles`, or any other top-level directories
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
```
> If you are using Expo Router, consider using a root **src** directory to simplify this step. Learn more about [top-level src directory](/router/reference/src-directory/).
Step 3: 
Create a **global.css** file in the root of your project and directives for each of Tailwind's layers:
```css global.css
/* This file adds the requisite utility classes for Tailwind to work. */
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Step 4: 
Import the **global.css** file in your **app/\_layout.tsx** (if using Expo Router) or **index.js** file:
```tsx
import '../global.css';
```
```tsx
// Import the global.css file in the index.js file:
import './global.css';
```
> **info** If you are using [DOM components](/guides/dom-components), add this file import to each module using the `"use dom"` directive since they don't share globals.
Step 5: 
You now start your project and use Tailwind CSS classes in your components.
```sh
$ npx expo start
```
For v4: 
Step 1: 
Install `tailwindcss` and its required peer dependencies:
```sh
$ npx expo install tailwindcss @tailwindcss/postcss postcss --dev
```
Step 2: 
Add Tailwind to your PostCSS configuration
```js postcss.config.mjs
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};
```
Step 3: 
Create a global CSS file that imports Tailwind CSS.
You can choose any name for this file. Using **global.css** is common practice.
```css global.css
@import 'tailwindcss';
```
Step 4: 
Import your CSS file in your **app/\_layout.tsx** (if using Expo Router) or **index.js** file:
```tsx
// If using Expo Router, import your CSS file in the app/_layout.tsx file
import '../global.css';
```
```tsx
// Otherwise import your CSS file in the index.js file:
import './global.css';
```
> **info** If you are using [DOM components](/guides/dom-components), add this file import to each module using the `"use dom"` directive since they don't share globals.
Step 5: 
You now start your project and use Tailwind CSS classes in your components.
```sh
$ npx expo start
```
## Usage
You can use Tailwind with React DOM elements as-is:
```tsx app/index.tsx
export default function Index() {
  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:
```tsx app/index.tsx
import { View, Text } from 'react-native';
export default function Index() {
  return (
    <View style={{ $$css: true, _: 'bg-slate-100 rounded-xl' }}>
      <Text style={{ $$css: true, _: 'text-lg font-medium' }}>Welcome to Tailwind</Text>
    </View>
  );
}
```
## Tailwind for Android and iOS
Tailwind does not support Android and iOS platforms. You can use a compatibility library such as [NativeWind](https://www.nativewind.dev/) for universal support.
## Alternative for Android and iOS
Alternatively, you can use [DOM components](/guides/dom-components) to render your Tailwind web code in a `WebView` on native.
```tsx app/index.tsx
'use dom';
// Remember to import the global.css file in each DOM component.
import '../global.css';
export default function Page() {
  return (
    <div className="bg-slate-100 rounded-xl">
      <p className="text-lg font-medium">Welcome to Tailwind</p>
    </div>
  );
}
```
## Troubleshooting
If you have a custom `config.cacheStores` in your **metro.config.js**, you need to extend the Expo superclass of `FileStore`:
```js metro.config.js
// Import the Expo superclass which has support for PostCSS.
const { FileStore } = require('@expo/metro-config/file-store');
config.cacheStores = [
  new FileStore({
    root: '/path/to/custom/cache',
  }),
];
module.exports = config;
```
Ensure you don't have CSS disabled in your **metro.config.js**:
```js metro.config.js
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname, {
  // Do not disable CSS support when using Tailwind.
  isCSSEnabled: true,
});
```


## Using local HTTPS development

Learn how to set up local HTTPS for Expo web apps.

When developing Expo web apps locally, you may need to use HTTPS with your local development environment for testing secure browser APIs. This guide shows you how to set up local HTTPS for Expo web apps.
## Prerequisites
This guide requires the following tool installed on your machine:
- `mkcert`: A tool for creating development certificates. For installation instructions, see the [`mkcert` GitHub repository](https://github.com/FiloSottile/mkcert#installation).
## Benefits
- **Team scalability**: Same setup works for everyone
- **Authentication support**: HTTP-Only Cookies and secure contexts
- **Production parity**: Match your production HTTPS environment
- **Easy sharing**: Consistent development URLs across the team
## Set up your project
Step 1: 
Create or navigate to your Expo project:
```sh
$ npx create-expo-app@latest example-app
$ cd example-app
$ cd your-expo-project
```
Step 2: 
Start your Expo development server:
```sh
$ npx expo start --web
```
Your app will be running on `http://localhost:8081`. Keep this terminal window open.
Step 3: 
Use `mkcert` to generate a certificate for localhost. Run the following command in a new terminal window from your project's root directory:
```sh
$ mkcert localhost
```
> **info** **Tip**: Ensure that after installing `mkcert`, you run `mkcert -install` to install the local certificate authority (CA).
This will generate two signed certificate files: `localhost.pem` (certificate) and `localhost-key.pem` (private key), inside your project's root directory.
Step 4: 
Inside your project's root directory, run the following command to start the proxy:
```sh
$ npx local-ssl-proxy --source 443 --target 8081 --cert localhost.pem --key localhost-key.pem
```
> **info** **Tip**: [`local-ssl-proxy`](https://github.com/cameronhunter/local-ssl-proxy) is a tool that creates a proxy server that forwards HTTPS traffic from port 443 to your Expo dev server on port 8081.
This creates a proxy that forwards HTTPS traffic from port 443 to your Expo dev server on port 8081.
Step 5: 
Open `https://localhost` in your browser to access your app. Your Expo app is now running with HTTPS.