Skillshub use-dom
Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/expo/skills/use-dom" ~/.claude/skills/comeonoliver-skillshub-use-dom-9a875f && rm -rf "$T"
skills/expo/skills/use-dom/SKILL.mdWhat 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 —
files cannot be DOM components_layout
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
- Must have
directive at the top of the file'use dom'; - 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
domEvery 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
dom// 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