Caracal Starter
UI & Theming

UI and Theming

How we manage the UI and theming of the application.

How we manage the UI and theming of the application.

Why Uniwind?

For the past few years, we have tried multiple approaches to style React Native apps: Stylesheet API, styled-components, restyle, and more.

Right now, we are confident that using Uniwind with React Native is the right solution. Uniwind provides a TailwindCSS-like experience specifically optimized for React Native.

If you are familiar with TailwindCSS on the web, you'll find Uniwind very easy to use, with a similar utility-first approach adapted for React Native's constraints.

Uniwind was a natural choice, considering that many developers come from a web background and have experience with TailwindCSS.

If you are not familiar with TailwindCSS, we recommend you read the TailwindCSS documentation first to understand the utility-first CSS approach.

You may also want to play with the TailwindCSS playground to get a better understanding of how utility classes work before using them in React Native.

About Uniwind

Uniwind is a utility-first styling library for React Native that provides a TailwindCSS-like experience. It uses the className prop to apply styles directly to React Native components, making it intuitive for developers familiar with TailwindCSS on the web.

Uniwind is designed specifically for React Native and works seamlessly with modern React Native architecture, including the new architecture and Expo.

For more details about Uniwind, check the official documentation.

Here is an example of how your component should look:

import { Card, Button } from 'heroui-native';
import { View, Text } from '@/components/ui';

export function ProfileCard() {
  return (
    <View className="p-4">
      <Card className="border border-neutral-200 dark:border-neutral-700">
        <Card.Header>
          <Text className="text-xl font-bold text-neutral-900 dark:text-neutral-100">
            User Profile
          </Text>
        </Card.Header>
        <Card.Body>
          <Text className="text-base text-neutral-600 dark:text-neutral-400">
            This card demonstrates Uniwind styling with dark mode support.
          </Text>
        </Card.Body>
        <Card.Footer>
          <Button variant="solid" color="primary">
            View Details
          </Button>
        </Card.Footer>
      </Card>
    </View>
  );
}

Configuration

Uniwind follows TailwindCSS configuration patterns, allowing you to customize themes and colors to match your design system.

Uniwind builds on TailwindCSS v4, which is configured in CSS — there is no tailwind.config.js file in this project.

global.css at the project root is the entry point. It pulls in Tailwind, Uniwind, the HeroUI Native styles, and the app theme:

global.css
@import "tailwindcss";
@import "uniwind";

@import "heroui-native/styles";

@import "./src/themes/sky.css";

Our design tokens live in src/themes/sky.css, declared as CSS custom properties inside @layer theme and split into @variant light and @variant dark blocks:

src/themes/sky.css
@layer theme {
  :root {
    @variant light {
      --background: oklch(97.02% 0.0048 225);
      --foreground: oklch(21.03% 0.0059 225);
      --accent: oklch(78% 0.16 225);
      /* ... */
    }

    @variant dark {
      /* same tokens, dark values */
    }
  }
}

To use your own palette, edit the token values in src/themes/sky.css (or add another theme file and import it from global.css). The tokens back Tailwind class names such as bg-background, text-foreground, and bg-accent, and HeroUI Native components read the same tokens, so both stay in sync.

You can read more in the Uniwind documentation and the TailwindCSS v4 theme docs.

Dark Mode

Why dark mode?

Dark mode has gained significant traction in recent years and has become an expected feature. By applying dark mode, it makes it easier on the eyes in low-light environments and reduces eye strain, which means more time spent on your app.

This template comes with dark mode support out of the box, and it's very easy to customize the color scheme of your app. Thanks to TailwindCSS dark mode.

Implementation

Uniwind applies the active theme, and we only take care of the colors. Every color is defined once per variant in src/themes/sky.css, so a component written with bg-background text-foreground automatically resolves to the right value in light and dark.

AppThemeProvider (src/lib/contexts/app-theme-context.tsx) wraps the app in src/app/_layout.tsx. It reads the current theme from Uniwind's useUniwind() hook and exposes it through the useAppTheme() hook:

const { currentTheme, isLight, isDark, setTheme, toggleTheme } = useAppTheme();

Under the hood setTheme and toggleTheme call Uniwind.setTheme(...).

How do we handle theme changes?

The useSelectedTheme hook (src/lib/hooks/use-selected-theme.tsx) is the one place that persists the user's choice. It stores the value in MMKV under the SELECTED_THEME key via useMMKVString, so the selection survives restarts, and falls back to 'system' when nothing is saved:

const { selectedTheme, setSelectedTheme } = useSelectedTheme();

setSelectedTheme('dark');

setSelectedTheme writes to storage and calls setTheme from useAppTheme() in the same step, so storage and the rendered color scheme never drift apart.

For a plain light/dark switch that does not need to be persisted, src/components/theme-toggle.tsx uses toggleTheme and isLight from useAppTheme() directly.

Use useSelectedTheme only for reading or changing the selection (a settings screen or toggle). To style a component based on the active theme, use useAppTheme() — or better, just write dark: variants in your className.

Add dark mode for each component

To add the values for the light mode, you can simply write them directly in your component class. For the dark mode, use the dark: variant.

<View className="... border-neutral-200 dark:border-yellow-700">....</View>

If you want to use the style prop, you can use the useColorScheme hook to get the current color scheme and apply the desired style. However, in most cases, you won't need it as the dark: variant will do the job.

import { useColorScheme } from 'react-native';

const colorScheme = useColorScheme();
const style =
  colorScheme === 'dark'
    ? { backgroundColor: 'black' }
    : { backgroundColor: 'white' };

For more details about dark mode, check the TailwindCSS dark mode documentation and Uniwind documentation.

On this page