| name | use-dom |
| description | Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally. |
| version | 1.0.0 |
| license | MIT |
What are DOM Components?
DOM components allow web code to run verbatim in a webview on native platforms while rendering as-is on web. This enables using web-only libraries like recharts, react-syntax-highlighter, or any React web library in your Expo app without modification.
When to Use DOM Components
Use DOM components when you need:
- Web-only libraries — Charts (recharts, chart.js), syntax highlighters, rich text editors, or any library that depends on DOM APIs
- Migrating web code — Bring existing React web components to native without rewriting
- Complex HTML/CSS layouts — When CSS features aren't available in React Native
- iframes or embeds — Embedding external content that requires a browser context
- Canvas or WebGL — Web graphics APIs not available natively
When NOT to Use DOM Components
Avoid DOM components when:
- Native performance is critical — Webviews add overhead
- Simple UI — React Native components are more efficient for basic layouts
- Deep native integration — Use local modules instead for native APIs
- Layout routes —
_layout files cannot be DOM components
Basic DOM Component
Create a new file with the 'use dom'; directive at the top:
'use dom'
export default function WebChart({
data,
}: {
data: number[]
dom: import('expo/dom').DOMProps
}) {
return (
<div style={{padding: 20}}>
<h2>Chart Data</h2>
<ul>
{data.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
</div>
)
}
Rules for DOM Components
- Must have
'use dom'; directive at the top of the file
- Single default export — One React component per file
- Own file — Cannot be defined inline or combined with native components
- Serializable props only — Strings, numbers, booleans, arrays, plain objects
- Include CSS in the component file — DOM components run in isolated context
The dom Prop
Every DOM component receives a special dom prop for webview configuration. Always type it in your props:
'use dom'
interface Props {
content: string
dom: import('expo/dom').DOMProps
}
export default function MyComponent({content}: Props) {
return <div>{content}</div>
}
Common dom Prop Options
<DOMComponent dom={{ scrollEnabled: false }} />
<DOMComponent dom={{ contentInsetAdjustmentBehavior: "never" }} />
<DOMComponent dom={{ style: { width: 300, height: 400 } }} />
<DOMComponent
dom={{
scrollEnabled: false,
contentInsetAdjustmentBehavior: "never",
style: { width: '100%', height: 500 }
}}
/>
Exposing Native Actions to the Webview
Pass async functions as props to expose native functionality to the DOM component:
import DOMComponent from '@/components/dom-component'
import {Alert} from 'react-native'
export default function Screen() {
return (
<DOMComponent
showAlert={async (message: string) => {
Alert.alert('From Web', message)
}}
saveData={async (data: {name: string; value: number}) => {
// Save to native storage, database, etc.
console.log('Saving:', data)
return {success: true}
}}
/>
)
}
'use dom'
interface Props {
showAlert: (message: string) => Promise<void>
saveData: (data: {name: string; value: number}) => Promise<{success: boolean}>
dom?: import('expo/dom').DOMProps
}
export default function DOMComponent({showAlert, saveData}: Props) {
const handleClick = async () => {
await showAlert('Hello from the webview!')
const result = await saveData({name: 'test', value: 42})
console.log('Save result:', result)
}
return <button onClick={handleClick}>Trigger Native Action</button>
}
Using Web Libraries
DOM components can use any web library:
'use dom'
import {docco} from 'react-syntax-highlighter/dist/esm/styles/hljs'
import SyntaxHighlighter from 'react-syntax-highlighter'
interface Props {
code: string
language: string
dom?: import('expo/dom').DOMProps
}
export default function SyntaxHighlight({code, language}: Props) {
return (
<SyntaxHighlighter
language={language}
style={docco}
>
{code}
</SyntaxHighlighter>
)
}
'use dom'
import {LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip} from 'recharts'
interface Props {
data: Array<{name: string; value: number}>
dom: import('expo/dom').DOMProps
}
export default function Chart({data}: Props) {
return (
<LineChart
width={400}
height={300}
data={data}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="value"
stroke="#8884d8"
/>
</LineChart>
)
}
CSS in DOM Components
CSS imports must be in the DOM component file since they run in isolated context:
'use dom'
import '@/styles.css'
export default function StyledComponent({
dom,
}: {
dom: import('expo/dom').DOMProps
}) {
return (
<div className="container">
<h1 className="title">Styled Content</h1>
</div>
)
}
Or use inline styles / CSS-in-JS:
'use dom'
const styles = {
container: {
padding: 20,
backgroundColor: '#f0f0f0',
},
title: {
fontSize: 24,
color: '#333',
},
}
export default function StyledComponent({
dom,
}: {
dom: import('expo/dom').DOMProps
}) {
return (
<div style={styles.container}>
<h1 style={styles.title}>Styled Content</h1>
</div>
)
}
Expo Router in DOM Components
The expo-router <Link /> component and router API work inside DOM components:
'use dom'
import {Link, useRouter} from 'expo-router'
export default function Navigation({dom}: {dom: import('expo/dom').DOMProps}) {
const router = useRouter()
return (
<nav>
<Link href="/about">About</Link>
<button onClick={() => router.push('/settings')}>Settings</button>
</nav>
)
}
Router APIs That Require Props
These hooks don't work directly in DOM components because they need synchronous access to native routing state:
useLocalSearchParams()
useGlobalSearchParams()
usePathname()
useSegments()
useRootNavigation()
useRootNavigationState()
Solution: Read these values in the native parent and pass as props:
import {useLocalSearchParams, usePathname} from 'expo-router'
import DOMComponent from '@/components/dom-component'
export default function Screen() {
const {id} = useLocalSearchParams()
const pathname = usePathname()
return (
<DOMComponent
id={id as string}
pathname={pathname}
/>
)
}
'use dom'
interface Props {
id: string
pathname: string
dom?: import('expo/dom').DOMProps
}
export default function DOMComponent({id, pathname}: Props) {
return (
<div>
<p>Current ID: {id}</p>
<p>Current Path: {pathname}</p>
</div>
)
}
Detecting DOM Environment
Check if code is running in a DOM component:
'use dom'
import {IS_DOM} from 'expo/dom'
export default function Component({dom}: {dom?: import('expo/dom').DOMProps}) {
return <div>{IS_DOM ? 'Running in DOM component' : 'Running natively'}</div>
}
Assets
Prefer requiring assets instead of using the public directory:
'use dom'
const logo = require('../assets/logo.png')
export default function Component({dom}: {dom: import('expo/dom').DOMProps}) {
return (
<img
src={logo}
alt="Logo"
/>
)
}
Usage from Native Components
Import and use DOM components like regular components:
import CodeBlock from '@/components/code-block'
import WebChart from '@/components/web-chart'
import {View, Text} from 'react-native'
export default function HomeScreen() {
return (
<View style={{flex: 1}}>
<Text>Native content above</Text>
<WebChart
data={[10, 20, 30, 40, 50]}
dom={{style: {height: 300}}}
/>
<CodeBlock
code="const x = 1;"
language="javascript"
dom={{scrollEnabled: true}}
/>
<Text>Native content below</Text>
</View>
)
}
Platform Behavior
| Platform | Behavior |
|---|
| iOS | Rendered in WKWebView |
| Android | Rendered in WebView |
| Web | Rendered as-is (no webview wrapper) |
On web, the dom prop is ignored since no webview is needed.
Tips
- DOM components hot reload during development
- Keep DOM components focused — don't put entire screens in webviews
- Use native components for navigation chrome, DOM components for specialized content
- Test on all platforms — web rendering may differ slightly from native webviews
- Large DOM components may impact performance — profile if needed
- The webview has its own JavaScript context — cannot directly share state with native