Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Official Expo team skills for building, deploying, and debugging Expo apps, optimized for Claude Code.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: use-dom3description: Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.4version: 1.0.05license: MIT6---78## What are DOM Components?910DOM 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.1112## When to Use DOM Components1314Use DOM components when you need:1516- **Web-only libraries** — Charts (recharts, chart.js), syntax highlighters, rich text editors, or any library that depends on DOM APIs17- **Migrating web code** — Bring existing React web components to native without rewriting18- **Complex HTML/CSS layouts** — When CSS features aren't available in React Native19- **iframes or embeds** — Embedding external content that requires a browser context20- **Canvas or WebGL** — Web graphics APIs not available natively2122## When NOT to Use DOM Components2324Avoid DOM components when:2526- **Native performance is critical** — Webviews add overhead27- **Simple UI** — React Native components are more efficient for basic layouts28- **Deep native integration** — Use local modules instead for native APIs29- **Layout routes** — `_layout` files cannot be DOM components3031## Basic DOM Component3233Create a new file with the `'use dom';` directive at the top:3435```tsx36// components/WebChart.tsx37"use dom";3839export default function WebChart({40data,41}: {42data: number[];43dom: import("expo/dom").DOMProps;44}) {45return (46<div style={{ padding: 20 }}>47<h2>Chart Data</h2>48<ul>49{data.map((value, i) => (50<li key={i}>{value}</li>51))}52</ul>53</div>54);55}56```5758## Rules for DOM Components59601. **Must have `'use dom';` directive** at the top of the file612. **Single default export** — One React component per file623. **Own file** — Cannot be defined inline or combined with native components634. **Serializable props only** — Strings, numbers, booleans, arrays, plain objects645. **Include CSS in the component file** — DOM components run in isolated context6566## The `dom` Prop6768Every DOM component receives a special `dom` prop for webview configuration. Always type it in your props:6970```tsx71"use dom";7273interface Props {74content: string;75dom: import("expo/dom").DOMProps;76}7778export default function MyComponent({ content }: Props) {79return <div>{content}</div>;80}81```8283### Common `dom` Prop Options8485```tsx86// Disable body scrolling87<DOMComponent dom={{ scrollEnabled: false }} />8889// Flow under the notch (disable safe area insets)90<DOMComponent dom={{ contentInsetAdjustmentBehavior: "never" }} />9192// Control size manually93<DOMComponent dom={{ style: { width: 300, height: 400 } }} />9495// Combine options96<DOMComponent97dom={{98scrollEnabled: false,99contentInsetAdjustmentBehavior: "never",100style: { width: '100%', height: 500 }101}}102/>103```104105## Exposing Native Actions to the Webview106107Pass async functions as props to expose native functionality to the DOM component:108109```tsx110// app/index.tsx (native)111import { Alert } from "react-native";112import DOMComponent from "@/components/dom-component";113114export default function Screen() {115return (116<DOMComponent117showAlert={async (message: string) => {118Alert.alert("From Web", message);119}}120saveData={async (data: { name: string; value: number }) => {121// Save to native storage, database, etc.122console.log("Saving:", data);123return { success: true };124}}125/>126);127}128```129130```tsx131// components/dom-component.tsx132"use dom";133134interface Props {135showAlert: (message: string) => Promise<void>;136saveData: (data: {137name: string;138value: number;139}) => Promise<{ success: boolean }>;140dom?: import("expo/dom").DOMProps;141}142143export default function DOMComponent({ showAlert, saveData }: Props) {144const handleClick = async () => {145await showAlert("Hello from the webview!");146const result = await saveData({ name: "test", value: 42 });147console.log("Save result:", result);148};149150return <button onClick={handleClick}>Trigger Native Action</button>;151}152```153154## Using Web Libraries155156DOM components can use any web library:157158```tsx159// components/syntax-highlight.tsx160"use dom";161162import SyntaxHighlighter from "react-syntax-highlighter";163import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";164165interface Props {166code: string;167language: string;168dom?: import("expo/dom").DOMProps;169}170171export default function SyntaxHighlight({ code, language }: Props) {172return (173<SyntaxHighlighter language={language} style={docco}>174{code}175</SyntaxHighlighter>176);177}178```179180```tsx181// components/chart.tsx182"use dom";183184import {185LineChart,186Line,187XAxis,188YAxis,189CartesianGrid,190Tooltip,191} from "recharts";192193interface Props {194data: Array<{ name: string; value: number }>;195dom: import("expo/dom").DOMProps;196}197198export default function Chart({ data }: Props) {199return (200<LineChart width={400} height={300} data={data}>201<CartesianGrid strokeDasharray="3 3" />202<XAxis dataKey="name" />203<YAxis />204<Tooltip />205<Line type="monotone" dataKey="value" stroke="#8884d8" />206</LineChart>207);208}209```210211## CSS in DOM Components212213CSS imports must be in the DOM component file since they run in isolated context:214215```tsx216// components/styled-component.tsx217"use dom";218219import "@/styles.css"; // CSS file in same directory220221export default function StyledComponent({222dom,223}: {224dom: import("expo/dom").DOMProps;225}) {226return (227<div className="container">228<h1 className="title">Styled Content</h1>229</div>230);231}232```233234Or use inline styles / CSS-in-JS:235236```tsx237"use dom";238239const styles = {240container: {241padding: 20,242backgroundColor: "#f0f0f0",243},244title: {245fontSize: 24,246color: "#333",247},248};249250export default function StyledComponent({251dom,252}: {253dom: import("expo/dom").DOMProps;254}) {255return (256<div style={styles.container}>257<h1 style={styles.title}>Styled Content</h1>258</div>259);260}261```262263## Expo Router in DOM Components264265The expo-router `<Link />` component and router API work inside DOM components:266267```tsx268"use dom";269270import { Link, useRouter } from "expo-router";271272export default function Navigation({273dom,274}: {275dom: import("expo/dom").DOMProps;276}) {277const router = useRouter();278279return (280<nav>281<Link href="/about">About</Link>282<button onClick={() => router.push("/settings")}>Settings</button>283</nav>284);285}286```287288### Router APIs That Require Props289290These hooks don't work directly in DOM components because they need synchronous access to native routing state:291292- `useLocalSearchParams()`293- `useGlobalSearchParams()`294- `usePathname()`295- `useSegments()`296- `useRootNavigation()`297- `useRootNavigationState()`298299**Solution:** Read these values in the native parent and pass as props:300301```tsx302// app/[id].tsx (native)303import { useLocalSearchParams, usePathname } from "expo-router";304import DOMComponent from "@/components/dom-component";305306export default function Screen() {307const { id } = useLocalSearchParams();308const pathname = usePathname();309310return <DOMComponent id={id as string} pathname={pathname} />;311}312```313314```tsx315// components/dom-component.tsx316"use dom";317318interface Props {319id: string;320pathname: string;321dom?: import("expo/dom").DOMProps;322}323324export default function DOMComponent({ id, pathname }: Props) {325return (326<div>327<p>Current ID: {id}</p>328<p>Current Path: {pathname}</p>329</div>330);331}332```333334## Detecting DOM Environment335336Check if code is running in a DOM component:337338```tsx339"use dom";340341import { IS_DOM } from "expo/dom";342343export default function Component({344dom,345}: {346dom?: import("expo/dom").DOMProps;347}) {348return <div>{IS_DOM ? "Running in DOM component" : "Running natively"}</div>;349}350```351352## Assets353354Prefer requiring assets instead of using the public directory:355356```tsx357"use dom";358359// Good - bundled with the component360const logo = require("../assets/logo.png");361362export default function Component({363dom,364}: {365dom: import("expo/dom").DOMProps;366}) {367return <img src={logo} alt="Logo" />;368}369```370371## Usage from Native Components372373Import and use DOM components like regular components:374375```tsx376// app/index.tsx377import { View, Text } from "react-native";378import WebChart from "@/components/web-chart";379import CodeBlock from "@/components/code-block";380381export default function HomeScreen() {382return (383<View style={{ flex: 1 }}>384<Text>Native content above</Text>385386<WebChart data={[10, 20, 30, 40, 50]} dom={{ style: { height: 300 } }} />387388<CodeBlock389code="const x = 1;"390language="javascript"391dom={{ scrollEnabled: true }}392/>393394<Text>Native content below</Text>395</View>396);397}398```399400## Platform Behavior401402| Platform | Behavior |403| -------- | ----------------------------------- |404| iOS | Rendered in WKWebView |405| Android | Rendered in WebView |406| Web | Rendered as-is (no webview wrapper) |407408On web, the `dom` prop is ignored since no webview is needed.409410## Tips411412- DOM components hot reload during development413- Keep DOM components focused — don't put entire screens in webviews414- Use native components for navigation chrome, DOM components for specialized content415- Test on all platforms — web rendering may differ slightly from native webviews416- Large DOM components may impact performance — profile if needed417- The webview has its own JavaScript context — cannot directly share state with native418