Set up react-strict-dom with Babel, PostCSS, and CSS-wrapped HTML components for universal Expo apps
This guide covers setting up react-strict-dom in an Expo project with Tailwind CSS v4 and react-native-css for universal app development.
react-strict-dom provides a subset of HTML/CSS that works identically across web and native platforms. This setup includes:
Install react-strict-dom using bun:
bun install react-strict-dom@0.0.54
Note: Version 0.0.55 has a broken dependency (
postcss-react-strict-dom@0.0.55doesn't exist). Use 0.0.54 until this is resolved.
Create or update babel.config.js in the project root:
const reactStrictPreset = require("react-strict-dom/babel-preset");
function getPlatform(caller) {
return caller && caller.platform;
}
function getIsDev(caller) {
if (caller?.isDev != null) return caller.isDev;
// https://babeljs.io/docs/options#envname
return (
process.env.BABEL_ENV === "development" ||
process.env.NODE_ENV === "development"
);
}
module.exports = function (api) {
//api.cache(true);
const platform = api.caller(getPlatform);
const dev = api.caller(getIsDev);
return {
presets: [
"babel-preset-expo",
[reactStrictPreset, { debug: true, dev, platform }],
],
};
};
debug: true - Enables debug output during developmentdev - Automatically detected from environmentplatform - Passed by Metro/bundler for platform-specific transformsUpdate postcss.config.mjs to include the react-strict-dom plugin:
export default {
plugins: {
"@tailwindcss/postcss": {},
"react-strict-dom/postcss-plugin": {
include: ["src/**/*.{js,jsx,mjs,ts,tsx}"],
},
},
};
The include option specifies which files to process for react-strict-dom styles.
Add the @react-strict-dom directive at the top of your global CSS file (e.g., src/global.css):
@react-strict-dom;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";
/* ... rest of your styles */
This directive must be the first rule in the file for react-strict-dom styles to work correctly.
Create CSS-wrapped HTML components that support Tailwind className at src/html/index.tsx:
import { useCssElement } from "react-native-css";
import React from "react";
import { html as rsd } from "react-strict-dom";
import { StyleSheet } from "react-native";
function createCssComponent<T extends keyof typeof rsd>(
elementName: T,
displayName?: string
) {
type Props = React.ComponentProps<(typeof rsd)[T]> & {
className?: string;
};
let Component: (props: Props) => React.ReactElement;
if (process.env.EXPO_OS === "web") {
Component = (props: Props) => {
// eslint-disable-next-line import/namespace
return useCssElement(rsd[elementName], props, {
// @ts-expect-error
className: "style",
});
};
} else {
Component = ({ style, ...props }: Props) => {
const normal = props as any;
if (style) {
normal.style = normalizeStyle(style);
}
return useCssElement(
// eslint-disable-next-line import/namespace
rsd[elementName],
normal,
{
// @ts-expect-error
className: "style",
}
);
};
}
(Component as any).displayName = displayName || `CSS(${elementName})`;
return Component;
}
function normalizeStyle(style: any) {
if (!style) return style;
const flat = StyleSheet.flatten(style);
if (flat.backgroundImage) {
flat.experimental_backgroundImage = flat.backgroundImage;
delete flat.backgroundImage;
}
return flat;
}
export const html = {
button: createCssComponent("button"),
div: createCssComponent("div"),
h1: createCssComponent("h1"),
h2: createCssComponent("h2"),
h3: createCssComponent("h3"),
h4: createCssComponent("h4"),
h5: createCssComponent("h5"),
h6: createCssComponent("h6"),
p: createCssComponent("p"),
span: createCssComponent("span"),
img: createCssComponent("img"),
input: createCssComponent("input"),
textarea: createCssComponent("textarea"),
a: createCssComponent("a"),
ul: createCssComponent("ul"),
ol: createCssComponent("ol"),
li: createCssComponent("li"),
nav: createCssComponent("nav"),
header: createCssComponent("header"),
footer: createCssComponent("footer"),
main: createCssComponent("main"),
section: createCssComponent("section"),
article: createCssComponent("article"),
aside: createCssComponent("aside"),
label: createCssComponent("label"),
form: createCssComponent("form"),
i: createCssComponent("i"),
b: createCssComponent("b"),
strong: createCssComponent("strong"),
em: createCssComponent("em"),
code: createCssComponent("code"),
pre: createCssComponent("pre"),
blockquote: createCssComponent("blockquote"),
hr: createCssComponent("hr"),
};
Import and use the HTML components with Tailwind classes:
import { html } from "@/html";
export function MyComponent() {
return (
<html.div className="flex flex-col gap-4 p-4 bg-sf-bg">
<html.h1 className="text-2xl font-bold text-sf-text">
Hello World
</html.h1>
<html.p className="text-sf-text-2">
This works on both web and native!
</html.p>
<html.button className="px-4 py-2 bg-sf-blue text-white rounded-lg">
Click me
</html.button>
</html.div>
);
}
On web, react-strict-dom renders actual HTML elements (<div>, <button>, etc.) with the PostCSS plugin processing styles. The useCssElement wrapper enables Tailwind className support.
On native, react-strict-dom transforms HTML elements into equivalent React Native components:
<div> → <View><span>, <p> → <Text><img> → <Image>The Babel preset handles these transformations at build time.
The normalizeStyle function handles platform-specific style differences:
StyleSheet.flatten()backgroundImage to experimental_backgroundImage for native compatibilityUse react-strict-dom@0.0.54 instead of the latest version. Version 0.0.55 has a missing dependency.
Ensure the @react-strict-dom directive is at the top of your global CSS file.
Use type assertions when setting displayName:
(Component as any).displayName = displayName;
The normalizeStyle function handles this automatically by mapping to experimental_backgroundImage.
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer