react-native-stylesheet-to-tailwind
v0.1.1
Published
Convert React Native StyleSheet.create styles to Tailwind className for Uniwind and NativeWind 5.
Maintainers
Readme
react-native-stylesheet-to-tailwind
A codemod that moves React Native StyleSheet.create styles to Tailwind className strings for Uniwind and NativeWind 5.
Try it in the browser: live playground.
It never guesses. Every class it writes is proven against your own Tailwind theme, so the app looks exactly the same after the run. A style that cannot be proven stays where it is, with a reason.
// before
<Text style={styles.title}>{title}</Text>
const styles = StyleSheet.create({
title: { marginTop: 2, fontSize: 16, fontWeight: "500", lineHeight: 20.8 },
})
// after
<Text className="mt-0.5 text-[16px] font-medium leading-[20.8px]">{title}</Text>Run it
npx react-native-stylesheet-to-tailwind # dry run: shows the diff, writes nothing
npx react-native-stylesheet-to-tailwind --write # writes the changes (needs a clean git tree)Run it in the app folder, or pass the folder as the first argument. It finds the library from package.json and the Tailwind CSS entry on its own.
| Flag | What it does |
| ----------------------------- | ------------------------------------------------------------------------------- |
| --write | Write the changes to the files. |
| --yes | Confirm --write when there is no terminal. |
| --allow-dirty | Write even when the git tree has changes. |
| --json | Print one JSON object. No colors, no prompts. |
| --diff | Show the diff also when writing. |
| --library <name> | uniwind or nativewind. Found from package.json when left out. |
| --css <path> | The Tailwind CSS entry file. Found when left out. |
| --class-merge <import path> | Import of your class-merge function, like @/cn. Lets conditional styles move. |
| --color-names <path> | JSON file that maps a color path in your code to a theme token name. |
| --no-arbitrary-values | Keep styles that would need classes like text-[17px]. |
| --include <glob> | Only convert matching files. Repeatable. |
| --exclude <glob> | Skip matching files. Repeatable. |
| --help, --version | Show help or the version. |
Exit codes: 0 ok, 1 failed, 2 bad usage.
How a class is proven
- It loads your Tailwind CSS entry with
@tailwindcss/node, so your theme tokens (text-brand,rounded-card) are known. - For each style it picks a candidate class, asks Tailwind for that class's CSS, and turns the CSS back into a React Native style.
- The class is written only when that style equals the original exactly. Otherwise the property stays in
StyleSheet.create.
A style can move fully, split (some properties move, the rest stay in style), or stay.
Color names
Pass --color-names colors.json to move colors that your code reads from a JS object. The file maps the color path, as written in the source, to a theme token name:
{ "colors.mutedForeground": "ink-soft", "palette.brand.main": "brand" }With that file, color: colors.mutedForeground becomes text-ink-soft, and backgroundColor and borderColor become bg- and border- classes. If the theme CSS has no --color-ink-soft, the style stays with color-name-not-in-theme. A bad file exits with code 2.
Why a style stays
| Reason | Meaning |
| --------------------------------- | ------------------------------------------------------------------------------------------- |
| component-has-no-class-name | The component does not take a className. |
| dynamic-value | The value is worked out at runtime. |
| used-outside-style-prop | The style is also read outside a style prop. |
| conditional-without-class-merge | The style is conditional and no class-merge function was found. |
| family-conflict | The classes would fight over the same property. |
| style-order-would-flip | Moving it would change which style wins. |
| no-exact-class | No Tailwind class gives the exact same value. |
| color-name-not-in-theme | The color is named in the color names file, but the theme CSS has no such --color- token. |
| scheme-wrapper | The styles are built by a function that reads the color scheme. |
| no-css-entry-covers-file | No working CSS entry of a Uniwind or NativeWind app covers this file. |
For agents
Moving styles by hand is a poor job for a coding agent: it reads every file, guesses each class, and spends tokens on work a tool can prove. Let the agent run the codemod and read one JSON object instead:
npx react-native-stylesheet-to-tailwind --json{
"mode": "dry-run",
"libraries": ["uniwind"],
"files": { "scanned": 9115, "changed": 520 },
"styles": { "moved": 258, "split": 721, "stayed": 2049, "arbitrary": 232 },
"stays": [
{
"file": "apps/web/app-frame.tsx",
"line": 29,
"rule": "frameStyles.frameOnWeb",
"reason": "no-exact-class"
}
]
}The agent then spends its tokens only on the stays list, where a human decision is needed.
SKILL.md is an agent skill with these steps and what to do for each stay reason. Add it to a project with npx skills add YosefHayim/react-native-stylesheet-to-tailwind.
Benchmark
Two agents (same model, same prompt goal) converted the same 10 StyleSheet files from a real Uniwind monorepo. One edited the files by hand, the other ran this codemod.
| | Manual agent | Codemod agent | Saved | | ------------------------ | ------------ | ------------- | ----- | | Tokens | 50,326 | 25,568 | 49% | | Time | 55.2 s | 18.0 s | 67% | | Tool calls | 8 | 3 | 63% | | Styles moved | 11 | 3 | | | Classes based on a guess | 4 | 0 | |
The manual agent moved more styles, but four of its classes were guesses. It wrote rounded-[10px] for a radius token that each app sets at runtime, and it replaced color-scheme style sets with bg-secondary, border-border and text-foreground without checking that those CSS colors match. The codemod left those styles in place with a reason (scheme-wrapper, dynamic-value, family-conflict), and every class it wrote was exact.
The codemod's cost is mostly the agent's fixed starting context, so the saving grows with the number of files. One run is a single sample, not a study; run your own with --json.
Library API
import { convertProject } from "react-native-stylesheet-to-tailwind";
const projectReport = await convertProject({
projectFolder: "./apps/mobile",
flags: {
write: false,
yes: false,
allowDirty: false,
json: true,
diff: false,
library: null,
cssEntryPath: null,
classMergeImport: "@/cn",
allowArbitraryValues: true,
includePatterns: [],
excludePatterns: [],
},
});Supported
- Uniwind and NativeWind 5 (Tailwind 4).
- Not yet: NativeWind 4 (Tailwind 3),
useColorSchemetodark:.
Node 20 or newer.
FAQ
No. A class is written only when the style Tailwind makes from it equals the original style exactly. Anything else stays in StyleSheet.create.
Each style that stays is listed with a reason. See Why a style stays. With --json they come in the stays list.
--write refuses to run on a git tree with uncommitted changes, unless you pass --allow-dirty. So the run is one clean diff: review it with git diff and undo it with git restore ..
Yes. Pass --include <glob> and --exclude <glob>, as many times as you need.
When no theme class gives the exact value, an arbitrary value class is the only exact match. Pass --no-arbitrary-values to keep those styles in StyleSheet.create instead.
A style like style={[styles.base, isActive && styles.active]} needs a class-merge function such as cn. The codemod uses the one your files import most, or the one you pass with --class-merge. Without one, the style stays with the reason conditional-without-class-merge.
Yes. Each file is checked against its own package and the Tailwind CSS entry that covers it.
Not yet. NativeWind 4 runs on Tailwind 3, which is not supported. Styles built inside a function that reads the color scheme stay with the reason scheme-wrapper.
No. Everything runs on your machine. The codemod makes no network calls.
Yes. --json prints one JSON object with no colors or prompts, and --write --yes writes without a terminal. See For agents and llms.txt.
License
MIT
