use-dom
Install this skill
npx skills add expo/skillsWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
The 'use dom' directive transforms Expo files into isolated webview-based components. By including this directive at the top of a file, developers can render standard HTML, CSS, and web-only React libraries directly within a native mobile application. This creates a bridge that executes web-specific code in a browser context while maintaining a consistent React API. It acts as an escape hatch for requirements that fall outside the capabilities of native UI primitives, such as specialized data visualizations or legacy web interfaces. Communication between the native host and the webview occurs via props, where asynchronous functions allow the web environment to trigger native system alerts or local storage operations, keeping the logic modular and contained within its own runtime environment.
When to Use This Skill
- β’Integrating complex charting libraries like Recharts or D3.js
- β’Embedding rich text editors or syntax highlighting components
- β’Reusing existing React web project components without modification
- β’Rendering dynamic content requiring browser-specific HTML/CSS features
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- βHow do I use a web-only library in Expo?
- βRender HTML or CSS components inside my native app
- βUse the use dom directive for webviews
- βBridge native functions to a webview component
- βCreate a component that runs in a webview context
Pro Tips
- π‘Prioritize performance-critical sections for native React Native components, using DOM components judiciously for less demanding, web-dependent features to balance performance and development speed.
- π‘When migrating, start with self-contained web components that have minimal native dependencies, gradually integrating them to identify and address any webview-specific quirks early.
- π‘Ensure robust cross-platform testing. While DOM components aim for 'as-is' rendering, differences in webview implementations across platforms (iOS vs. Android) can sometimes necessitate minor adjustments.
What this skill does
- β’Execute arbitrary web-only React libraries and components natively
- β’Isolate web-based rendering in a controlled browser context
- β’Bridge native host logic to webview components via async props
- β’Configure viewport behavior like scrolling and safe area insets via the dom prop
- β’Maintain native-like component syntax for hybrid applications
When not to use it
- βWhen high-performance animations or deep native UI integration are required
- βWhen standard React Native core components can fulfill the UI requirement
- βFor top-level layout routing structures
Example workflow
- Add 'use dom'; to the top of your component file
- Define your component using standard HTML tags and web libraries
- Import the DOMProps type to handle specific webview configurations
- Pass native functions as props to the component for bridge communication
- Include the component in your app's view hierarchy like a standard component
Prerequisites
- βExpo project environment
- βBasic understanding of React web development
Pitfalls & limitations
- !Webview overhead can impact performance compared to native components
- !Components must reside in their own files and cannot be defined inline
- !Props passed to the component must be serializable
- !Cannot use layout route files as DOM components
FAQ
How it compares
Unlike standard iframes or manual webview wrappers, 'use dom' provides a native React component interface, allowing for type-safe prop passing and native-to-web communication without manual message event listeners.
π Full skill instructions β original source: expo/skills
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:// components/WebChart.tsx
"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
1. **Must have
'use dom'; directive** at the top of the file2. **Single default export** β One React component per file
3. **Own file** β Cannot be defined inline or combined with native components
4. **Serializable props only** β Strings, numbers, booleans, arrays, plain objects
5. **Include CSS in the component file** β DOM components run in isolated context
## The
dom PropEvery 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// Disable body scrolling
<DOMComponent dom={{ scrollEnabled: false }} />
// Flow under the notch (disable safe area insets)
<DOMComponent dom={{ contentInsetAdjustmentBehavior: "never" }} />
// Control size manually
<DOMComponent dom={{ style: { width: 300, height: 400 } }} />
// Combine options
<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:
// app/index.tsx (native)
import { Alert } from "react-native";
import DOMComponent from "@/components/dom-component";
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 };
}}
/>
);
}// components/dom-component.tsx
"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:
// components/syntax-highlight.tsx
"use dom";
import SyntaxHighlighter from "react-syntax-highlighter";
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
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>
);
}// components/chart.tsx
"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:
// components/styled-component.tsx
"use dom";
import "@/styles.css"; // CSS file in same directory
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:
// app/[id].tsx (native)
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} />;
}// components/dom-component.tsx
"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";
// Good - bundled with the component
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:
// app/index.tsx
import { View, Text } from "react-native";
import WebChart from "@/components/web-chart";
import CodeBlock from "@/components/code-block";
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
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/use-dom/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/expo/skills/use-dom/SKILL.md - Cursor:
~/.cursor/skills/expo/skills/use-dom/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/expo/skills/use-dom/SKILL.md
π Install with CLI:npx skills add expo/skills