DEV Community

Cover image for How We Got an LLM to Reliably Generate Theme-Aware React Native Apps
Hugo Rus
Hugo Rus

Posted on • Originally published at rapidnative.com

How We Got an LLM to Reliably Generate Theme-Aware React Native Apps

  • Asking an LLM to "generate a React Native screen with dark mode" gives you inconsistent results — useColorScheme on one screen, a dark: prefix on another, a hardcoded bg-white on a third.
  • The fix isn't a better prompt. It's moving theme switching out of the model's job and into the project scaffold.
  • Every generated app starts from a theme.ts (light + dark via NativeWind v4 vars()), a root ThemeProvider, and a tailwind.config.js binding semantic class names to CSS variables.
  • Five system-prompt rules keep the model writing className="bg-background" and nothing else.
  • You export a clean Expo project with no runtime dependency on us.

How We Got an LLM to Reliably Generate Theme-Aware React Native Apps

If you've ever asked an LLM to "generate a React Native screen with dark mode support," you know the result. Sometimes you get useColorScheme. Sometimes you get a dark: prefix. Sometimes you get a hardcoded bg-white. Sometimes you get all three in the same file.

This post is about how we built theme-aware UI as a property of every generated app in RapidNative — an AI mobile app builder — instead of leaving it as a per-prompt gamble. We'll cover the prompt rules, the scaffold the model writes into, and the architectural choices that made dark mode boring and predictable. (Which, in this context, is high praise.)

The honest version of the problem

For human developers, the standard dark mode recipe in React Native is well documented: call useColorScheme, branch on the result, persist the user's preference, tint the status bar with expo-status-bar. Books have been written.

For LLMs generating code, that recipe is a minefield:

  • The model has to remember the pattern on every generated screen.
  • It has to apply it consistently to components it's never seen together.
  • It has to handle native primitives (ActivityIndicator, RefreshControl, Switch) that take hex colors, not class names.
  • It has to not invent new variations of the pattern (which it loves to do).

In our early experiments, the failure mode was always the same: one screen looked perfect in dark mode, the next one looked unreadable, and the third one had bg-white hardcoded because the model decided that screen "needed to feel clean."

The structural fix: theme switching as a foundation, not a per-component decision

We stopped trying to make the model handle color scheme detection. Instead, we made every generated project start from a scaffold that includes:

  • A theme.ts file with two named exports — lightTheme and darkTheme — each declared via NativeWind v4's vars() helper, mapping semantic CSS variable names to RGB triplets.
  • A ThemeProvider.tsx that picks the right vars object and applies it to the root view.
  • A tailwind.config.js set up so class names like bg-background and text-foreground resolve to those CSS variables at runtime.

Now the model never writes color scheme logic. It writes <View className="bg-background"> and trusts the foundation to handle the switch.

The system prompt rules that make it stick

The hard part isn't designing the scaffold. It's getting the LLM to use it correctly every time. Our system prompt encodes five rules that, taken together, get us a near-100% hit rate on theme correctness across generated screens.

Rule 1: semantic classes only. No dark: prefix. No bracket values like bg-[#ffffff]. No hardcoded hex in className.

Rule 2: theme.ts is fill-in-the-blanks. The model can change RGB values; it cannot restructure the file, rename exports, or change variable keys. This invariant is what lets our editor parse it and our theme editor UI work.

Rule 3: ThemeProvider is off-limits. Specifically: never pass value={...} to it. LLMs love passing reasonable-looking props. This one would silently break theme switching, so we forbid it by name with the consequence spelled out.

Rule 4: inline hex only from theme.ts. When a native primitive needs a color prop, the model derives the hex from theme.ts's RGB values using an isDark ternary — never a literal.

Rule 5: don't add unrequested variables. Including useColorScheme. The model is told explicitly not to inject color scheme detection that the user didn't ask for, because the system handles it.

Each rule on its own is small. Together they collapse a huge surface area of failure modes into "the model writes semantic class names, full stop."

Deterministic brand palettes

Every project gets a unique palette derived from a hash of its project ID. We pick a brand hue and accent hue, then generate matched light and dark palettes via HSL-to-RGB conversion. The model receives a pre-composed theme.ts it's told to emit verbatim on the first generation, so the brand identity stays consistent as the project grows.

The two-mode palette is generated together, from the same hue, in the same color space. That's the trick to making dark mode feel like the same product as light mode instead of an afterthought.

The export

When you download a project, you get a clean Expo workspace with theme.ts, the provider, NativeWind configured, and every screen using semantic classes. No runtime dependency on us. It's a standard React Native + Expo project that happens to have well-organized theming because the AI was given strict rules about how to build it.

What I'd take from this for any AI-codegen project

If you're shipping an LLM-powered code generator, three things from this approach generalize:

  1. Move correctness into the scaffold, not the prompt. Anything the foundation can enforce structurally is one less thing the model can mess up per call.
  2. Forbid patterns by name with consequences. Don't say "use semantic classes." Say "no dark: prefix, no hex in className." The model responds to specific bans much better than positive guidelines.
  3. Make file shape an invariant. If your post-processing tools (parsers, editors, watchers) depend on file structure, lock that structure in the prompt. The model will respect it if you're explicit.

Full deep-dive on the architecture (three theming layers, the brand-palette directive, the in-editor theme parser, the live preview, and what shows up in the export ZIP) is on our blog: How RapidNative Generates Theme-Aware React Native UIs.

What's the worst LLM-generated theming mess you've had to clean up? Drop a comment with what you're building.

Top comments (0)