How RapidNative Generates Theme-Aware React Native UIs

SA

By Suraj Ahmed

29th Jun 2026

Last updated: 29th Jun 2026

How RapidNative Generates Theme-Aware React Native UIs

Most "dark mode in React Native" guides teach you the same five things: call useColorScheme(), wire up a Context provider, swap colors based on the scheme, persist the user's choice, and don't forget the status bar. They're correct, and they're also where the real problem starts.

Because when an AI generates the code, none of that is automatic. The model has to understand that an app should respond to light and dark mode in the first place — and then it has to write code that does so consistently across every screen, every component, every inline style prop, and every native primitive that doesn't accept className. One slip and you get a screen that looks fine in dark mode and unreadable in light mode (or vice versa). Multiply that across a 12-screen generated app and the cracks show up everywhere.

This post walks through the actual approach we built into RapidNative — an AI mobile app builder that turns plain English into React Native code — to make theme-aware UI the default, not the exception. We'll cover the three-layer architecture, the prompt rules we hand the LLM, the design decisions that let users skip writing color-scheme logic entirely, and what the exported code actually looks like when you download a project.

Phone displaying a mobile app in dark mode on a desk Theme-aware UI means the same screen renders correctly in both modes without conditional logic in every component — Photo by UX Indonesia on Unsplash

What does theme-aware mean in a React Native app?

A theme-aware React Native UI is one where every visible element — backgrounds, text, borders, icons, status bar tint, even native modal overlays — adapts to the user's selected color scheme without per-screen conditionals. In practice this means a single set of semantic color tokens (like background, foreground, card, border) that resolve to different RGB values in light versus dark mode, with the switch handled at the root of the app instead of inside every component. It's the difference between writing <View className="bg-background"> once and writing <View style={{ backgroundColor: isDark ? '#191919' : '#ffffff' }}> in 40 places.

The problem with "just use useColorScheme"

The standard answer for dark mode in React Native is the useColorScheme hook — read the OS color scheme, branch on it, apply the right color. It works for hand-written code. It falls apart for AI-generated code for three reasons:

  1. The model has to remember to do it every time. Every new component, every new screen, every refactor. Miss one and you get a half-themed app.
  2. It encourages conditional clutter. isDark ? colors.darkBg : colors.lightBg everywhere — twice the surface area, twice the bug count.
  3. It doesn't help with native primitives. Components like ActivityIndicator, RefreshControl, and Switch take a color prop, not a className. They need hex values regardless of how the rest of the app is styled.

So we made a different choice: take dark mode away from the AI as a per-component decision and move it to the foundation of the generated project. The model never branches on color scheme. It writes semantic class names. The theme system handles the rest.

Developer writing React Native code on a laptop Generated code stays clean when the theme system handles the light/dark switching instead of every component — Photo by Lukas Blazek on Unsplash

The three-layer architecture

RapidNative has a theming system at three distinct layers, and it helps to keep them separate before getting into the code:

LayerWhere it livesWhat it themes
Marketing sitenext-themes + Tailwind CSS variablesThe rapidnative.com public site (light by default)
Studio (editor)next-themes + studio-specific CSS variablesThe authenticated app where you build projects (dark by default)
Generated appNativeWind v4 vars() + a scaffolded theme.tsThe React Native app the AI produces for you

The first two are standard Next.js patterns. The third is where the interesting work happens — that's the layer the AI actually generates code into. We'll cover all three, but most of this post is about layer three.

Layer 1: the marketing site

The website uses next-themes wired up in src/shared/components/RootProviders.tsx. The provider is configured with attribute="class" so the active theme is reflected as a class on <html>, defaultTheme="dark" (a deliberate choice for the brand), enableSystem={false} to ignore the OS scheme on landing, and a custom storageKey="rapidnative-theme".

The marketing routes under src/app/(site)/ then override that default back to light via a sync component (src/app/(site)/SiteThemeSync.tsx) that listens to resolvedTheme from useTheme() and toggles the class on document.documentElement. The result: the public site reads as a clean light surface, and the editor reads as the dark working environment most developers prefer.

Layer 2: the studio editor

The studio layout (src/app/(studio)/layout.tsx) loads a separate stylesheet — src/app/studio-globals.css — that defines the editor's own light and dark palettes in OKLCH color space, the perceptually uniform color space we adopted to keep contrast consistent across hues. A second sync component (src/app/(studio)/StudioDarkMode.tsx) handles the studio's default-to-dark behavior and — critically — injects a tiny inline script that runs before React hydrates. That script reads the persisted theme from localStorage and applies the class to <html> immediately, eliminating the flash of unstyled content that otherwise appears on first paint.

This is a small detail with a big payoff: nothing kills the perception of a polished product faster than a white flash on every page load in dark mode.

Layer 3: the generated app

This is the one users actually take with them. When the AI generates a project, every new app starts from a scaffold that includes:

  • theme.ts — a single file with two exports, lightTheme and darkTheme, each declared via NativeWind v4's vars() helper
  • src/providers/ThemeProvider.tsx — a scaffolded provider that wires the active theme to the root view (the AI is forbidden from editing this)
  • tailwind.config.js — NativeWind v4 configuration that maps semantic class names to the CSS variables in theme.ts

When a screen needs a background color, the AI writes className="bg-background". NativeWind resolves bg-background to var(--background) at runtime. --background is one set of RGB values when lightTheme is active and another when darkTheme is active. The screen renders correctly in both modes — no conditionals, no useColorScheme, no per-component branching.

How we instruct the AI to generate theme-aware code

Most posts on AI code generation stop at "the model writes the code." That's underselling what actually happens. A working system prompt for theme-aware generation has to do four things at once: explain the system, forbid the wrong patterns, define the surface area the AI can touch, and provide enforcement examples.

Here's the shape of the rules we encode in our system prompt (lives in tools/project-templates/fullstack/ai/prompts/system-prompt.ts):

Semantic-first styling

"Use semantic classes — they auto-adapt to light/dark. No dark: prefix, no arbitrary bracket values in className, no hardcoded hex in className."

The model is explicitly told that semantic tokens (bg-background, text-foreground, bg-card, text-muted-foreground, border-border) are the only acceptable way to express color in className. This single rule eliminates the entire class of bugs where one screen uses bg-white (which never adapts) and another uses bg-background (which does).

Theme.ts as a fill-in-the-blanks file

"theme.ts is a fill-in-the-blanks file — only replace the RGB values. Keep every import, every export name (lightTheme and darkTheme), every CSS variable key character-for-character identical to the scaffold."

The model can change colors. It cannot restructure the file. This invariant is what lets the rest of the system — the parser in the editor, the export pipeline, the theme editor UI — assume a predictable shape.

ThemeProvider is off-limits

"src/providers/ThemeProvider.tsx — no value prop. Calling <ThemeProvider value={...}> is invalid and breaks dark mode."

LLMs love to pass props that look reasonable. A value prop on a context provider looks reasonable. It would also silently destroy theme switching. So we forbid it by name, with the consequence spelled out.

Inline color rule for native primitives

"When a style prop requires a hex value (tab bar, RefreshControl tintColor, ActivityIndicator, Switch, Modal overlay), derive the hex from YOUR theme.ts RGB values and use the isDark ternary."

This is the one case where the model does need to branch — RefreshControl and friends don't accept className, so a hex color computed from the active theme is the only option. We allow it, but only when it's the only option, and only with values that come from theme.ts (so they stay in sync if the user customizes the palette later).

No unrequested variables

"Do not add new variables (useColorScheme, SplashScreen.hideAsync, etc.) that the user didn't ask for."

The model is told, in those words, not to inject useColorScheme calls. This keeps generated screens free of the boilerplate that competing tools tend to scatter everywhere.

The result of these five rules together is that across hundreds of generated screens, the color story stays identical: semantic classes everywhere, theme switching at the root, and the rare inline color always grounded in the user's actual palette.

Two phones showing the same app interface in light and dark modes side by side The same screen in light and dark mode — possible because color decisions live in one place, not 40 — Photo by Daniel Romero on Unsplash

Brand-tinted palettes: deterministic dark mode color generation

There's a subtler problem most app builders skip entirely: every generated app shouldn't look the same. If you ask for a fitness app, the brand should lean energetic. If you ask for a meditation app, it should feel calm. And the dark mode palette should hold the same character as the light one — same brand hue, same accent — not just inverted grays.

We handle this in a module called the GLM design directive (tools/project-templates/fullstack/ai/prompts/glm-design-directive.ts). When a project is created, we derive a brand hue and accent hue from a hash of the project ID — deterministic, so the palette stays stable across regenerations, but unique per project. From those two hues, we generate matched light and dark palettes via HSL-to-RGB conversion, and we hand the model a pre-composed theme.ts it's told to emit verbatim on the first generation.

The directive also assigns the project one of eight predetermined visual styles — editorial, bento grid, full-bleed media, minimalist list, data-dense, soft rounded consumer, brutalist, or glass/layered depth — and the system prompt enforces consistency with that style on every new screen. So a project that started in "soft rounded consumer" stays soft and rounded as it grows; one that started in "brutalist" stays brutalist.

Why this matters for dark mode specifically: lots of dark themes look fine in isolation but feel disjointed when paired with their light counterparts. Generating both from the same hue, in the same color space, in the same call, keeps the two modes feeling like one product instead of two near-strangers.

The theme editor: customize colors after generation

Generated palettes are a starting point, not a contract. Users frequently want to tweak the brand color, soften a border, or push the dark background a shade warmer. The Theme Editor (src/modules/studio/components/custom/editor/ThemeEditor/ThemeEditorPanel.tsx) is the UI for that.

Under the hood, the editor:

  1. Parses theme.ts with a regex-based parser (themeParser.ts) that pulls out the lightTheme and darkTheme blocks, the radius value, and the individual color entries.
  2. Renders two grids — one for light mode colors, one for dark mode — with OKLCH/RGB color pickers per token.
  3. Tracks state in a dedicated Redux slice (src/modules/editor/store/slices/themeEditorSlice.ts) with actions like updateColor, updateRadius, addColor, and deleteColor.
  4. Writes back to theme.ts on save, which triggers the project's virtual file system to rebuild and the live preview to reload — typically in under a second.

Because the parser depends on the file's exact scaffolded shape, the "don't restructure theme.ts" rule we feed the AI isn't just defensive — it's what makes interactive editing possible at all. If the model ever rewrote the file, the editor would lose its handle on it.

The live preview: seeing both modes immediately

A theme system that only works in production is useless during iteration. The in-editor preview (src/modules/studio/components/custom/editor/ProjectPreview.tsx) renders generated apps in an iframe using our custom in-browser React Native runtime, with a toggle that flips between light and dark instantly. Every change to theme.ts propagates through the file watcher to the iframe in real time — change a color, see it everywhere, in both modes, in seconds.

This loop is part of why the prompt rules matter so much. If a screen used bg-white instead of bg-background, the preview would expose it immediately when you flipped to dark mode. Because every AI-generated screen uses semantic tokens, that exposure rarely happens — and when it does, it's a regression worth investigating, not the daily reality.

What you actually get when you export

When you download a project from RapidNative, the export pipeline (src/app/api/user/projects/[projectId]/download/route.ts) bundles a complete React Native + Expo workspace including:

  • theme.ts with your current light and dark colors
  • src/providers/ThemeProvider.tsx — the scaffolded provider, unchanged
  • tailwind.config.js — NativeWind v4 config that binds class names to your CSS variables
  • All app screens using semantic classes that resolve correctly in both modes
  • package.json with nativewind, tailwindcss, expo, and the supporting dependencies pinned

The exported app is yours. No runtime dependency on RapidNative, no proprietary theming layer, no lock-in. 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.

This connects to a broader principle: the value of an AI app builder isn't the chat interface. It's the structural quality of the code it produces. We wrote about this from a different angle in our take on why AI-generated code beats React Native templates, and the theme system is one of the clearest examples of it in practice.

How this compares to the standard approaches

For developers who've implemented dark mode by hand, here's where our approach sits relative to the common patterns:

ApproachWhat it solvesWhat it leaves to you
useColorScheme + conditional colorsOS scheme detectionPersistence, every component's branching, native-primitive colors
React Navigation themesNavigator background and header colorsScreen content colors, custom components
RestyleTyped theme tokens, variantsWiring up every component, manual theme switching
NativeWind v4 vars()Class-based theming with CSS variablesDefining the palette, structuring the file, instructing your team to use it
RapidNative's approachAll of the above, generated and enforced by the AINothing — the scaffold ships ready, and the rules keep new code consistent

The last row is the only one where the theme system is created for you and policed by the generator itself. That's the entire point: dark mode stops being a project you start and becomes a property of every project you generate.

What this unlocks

Three concrete things become easier when theme-aware UI is the default:

  1. Cross-team handoff. A founder generates a fitness app and sends the export to a contractor. The contractor doesn't have to add a dark mode story — it's already there, on every screen, ready to ship.
  2. Brand changes without rewrites. Swap the primary hue in theme.ts and the whole app reflows to the new brand. Both light and dark.
  3. Lower regression risk. With color decisions in one file, you don't have a 12-screen audit every time you change a brand color. You have one diff.

We've found that the projects users invest in long-term are almost always the ones with clean theming, because that's the proxy for "this is structured enough that I trust extending it." A messy theme story is usually a sign that the rest of the code will be messy too.

Build a theme-aware app from a prompt

If you want to see this in action, start a project on RapidNative and ask for something like "a habit tracker with a clean minimalist aesthetic, soft purples, and a dark mode that feels calming." You'll get back a React Native + Expo project with both palettes wired up, every screen rendering correctly in both modes, and a theme.ts you can tweak in the in-editor theme editor or by hand after exporting.

You can also explore other slices of the same generation pipeline — like how we generate NativeWind styles from natural language, how the export pipeline ships your app to the App Store, or how the visual point-and-edit system works.

The theme system is one piece of a larger philosophy: the best part of an AI app builder isn't the moment of generation. It's everything the generated code lets you do afterwards. Theme-aware UI is one of the clearest examples — the AI does the boring part once, correctly, and you spend your time on the part that's actually yours.

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently asked questions

What is RapidNative?

RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.

Can I export the code?

Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.

Is RapidNative free to use?

Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.

Do I need to know how to code?

No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.

How long does it take to build an app?

Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.