A Guide to Tailwind CSS in React Native

React Native is very behind the web when it comes to UI libraries. There’s two likely reasons for this: CSS on the web is easy and the object known as StyleSheet in React Native is not.

At a fundamental level, the syntax of StyleSheet causes a lot of problems. It looks like CSS (the kind you’d use in React’s style prop), it acts like CSS (most of the time) just like you’d expect in a browser, but then you encounter one of dozens of scenarios where React Native takes the CSS specification and yeets it out the nearest window.

  • Shadows are a custom object syntax instead of a conversion of box-shadow
  • Random elements such as ScrollView introduce custom non-standard props such as elevation, but also only for specific platforms
  • You get flexbox. Only flexbox. Everything flexes. Everything is boxes. Except for the flex property which has decided it is not actually flex as you’d expect but just a number that translates to a percentage. But not a useful number-to-percentage like 55 meaning 55%. Instead, you get an alternate universe number that is expressed as a ratio based on the sum of all flex properties on sibling nodes. It’s a universe where flex:1 means both 100% and 50% based on elements outside of its rendering tree. /rant

Yes, a lot of this will be fixed over time as React Native continues to improve on its style interpreter, but we are still converting from CSS to platform-specific layout calls. I don’t think we’ll ever reach parity, but I’d also like to avoid the programming equivalent of “hammering my thumb” every time I lay things out in a mobile app.

The truth is, what I want is something as easy as Tailwind CSS in React Native. If you haven’t tried Tailwind yet, you should. It’s trivial to make something look good, and it’s adaptable enough withstand design changes.

Enter tailwind-rn-gust (MIT, npm) and its underlying library, tailwind-rn (MIT, npm). In concert, the two libraries give you Tailwind classes in your className prop. If you’re just wanting a more useful StyleSheet or style prop, check out tailwind-rn, and if you’d like a wrapper that gives you access to a more Tailwind-like place for your class names, that’s where tailwind-rn-gust comes in. Since tailwind-rn has been around for a while, this is about adding gust 🍃 and improving your Tailwind experience on React Native even further.

Utility Classes Make it Easy to do the Right Thing

My first reaction to Tailwind was a visible cringe. Do we really need flex-row as a class to set flex-direction: row? For that matter, how different is

<View className=“flex-row”>{/* … */}</View>

from its original React Native version

<View style={{
  flexDirection: “row”
}}>{/* … */}</View>

The answer to that is “not very.” In fact, the extra indirection of className might cause more confusion since CSS classes don’t technically exist. However, if we want to use a color and conditionally change it based on the user’s dark mode settings…

<View className=“flex-row bg-blue-100 dark:bg-blue-900”>
  {/* … */}
 </View>

and again in React Native…

<View style={{
  flexDirection: “row”
  backgroundColor: !isDarkMode? colors.blue.100 : colors.blue.900
}}>{/* … */}</View>

It’s getting noticeably more complex in React Native, while the API for Tailwind remains straightforward. If we introduced another variant (a premium user in dark mode for example) you’d have a horrible mess of ternary operators or your logic would be so far removed from the relevant component that debugging becomes difficult.

Meanwhile, in Tailwind bliss…

<View className=”
  flex-row
  bg-blue-100 premium:bg-yellow-100
  dark:bg-blue-900 premium:dark:bg-yellow-900
 ”>
  {/* … */}
 </View>

And that is why React Native benefits from the Tailwind API.

tailwind-rn-gust: Tailwind className in React Native

tailwind-rn-gust (MIT) is a small library designed to build on top of Tailwind and expose Tailwind classes via the traditional className prop in React Native. To do this, the library exposes

  • A higher order component that wraps a React Native component and exposes className and translates that into a call to the underlying tailwind-rn library
  • Support for active:, hover:, focus: and any user-generated pseudo class names
  • A React Hook, useEvent that simplifies the process of binding a set of React Native events (usually exposed as props on a component) to a stateful set of flags for enabling those pseudo class names above

Each of those items can be used alone or together to enhance your Tailwind CSS support in your React Native app. For example, you may just want the new tailwind(classNames, [flags]) function based on Matt Apperson’s original proposal which gives you support for the pseudo class names. Alternatively, you may just want to use className instead of style since that lowers the barrier to entry for your team.

Or, you may be all-in on the power of hooks and wished someone would make it easy to tie React Native props like onFocus and onBlur into Tailwind.

import React, { useMemo } from “react”;
import { gust, useEvent } from “tailwind”;

// generic React Native Component
const MyComponent = () => {};

const MyNewComponent = gust(MyComponent, null, (props) => {
  // (1) Get an active state. Generate listeners
  const [active, activeListeners] = useEvent([“onFocus”, “onBlur”], props);

  // (2) Memoize the result and return an array
  // [0] is your collection of flags
  // [1] is your collection of event listeners as React props
  const result = useMemo(
    () => [{ active, focus: active }, { …activeListeners }],
    [active, activeListeners]
  );

  // (3) Gust takes care of the state management & proxying callbacks
  return result;
});

const FunctionalComponent = () => {
  return <MyNewComponent className=“bg-blue-500 active:bg-red-500” />;
};

Is This Overkill?

This is mentioned specifically in the tailwind-rn-gust Anti-Pitch, but this library might be overkill if you’re just looking to interpret something like Pressable’s state which is available via a functional child. Gust also doesn’t offer up any opinions on how to generate the Tailwind flags such as md: (breakpoints) or dark: (appearance). The truth is, there are dozens of libraries in React Native and they all have different APIs, different providers, and different hooks.

You might also be insanely happy with NativeBase, UI Kitten, or one of the many other excellent React Native UI libraries out there. If you are, don’t switch. Having your design system spread across two systems is a pain I wouldn’t wish on anyone.

Getting Started in Five Minutes

If you just want to give gust and Tailwind a try in React Native, you can get started with gust in under five minutes. First up, these commands:

# ### 1 ###
yarn install tailwind-rn-gust tailwind-rn
# or npm install tailwind-rn-gust tailwind-rn

# ### 2 ###
npx tailwindcss init

# ### 3 ###
npx create-tailwind-rn

# ### 4 ###
touch tailwind.js
  1. Install tailwind-rn-gust and its peer dependency, tailwind-rn
  2. Init Tailwind CSS, which creates your default tailwind.config.js file
  3. Create a styles.json from Tailwind, putting the CSS in an intermediate state React Native can work with
  4. Create your local tailwind.js file which is where you’ll expose gust, tailwind, and your utility functions
import { create } from “tailwind-rn”;
import wrap, { useEvent } from “tailwind-rn-gust”;
import styles from “./styles.json”;

const { tailwind, getColor, gust } = wrap(create(styles));
export { tailwind, getColor, gust, useEvent };

Without any further work, you’ll have access to a tailwind(classNames, [flags]) function that converts a set of Tailwind CSS class names to React Native styles. A second, optional, parameter flags lets you turn “on” and “off” your Tailwind prefixes such as dark:, active:, or hover:.

And if you’d like your View to support className like in our example above?

import {View as RNView} from “react-native”;
import { gust } from “tailwind”;

export const View = gust(RNView, null, () => {
  // insert logic for determining “dark” and “premium”
  return {};
});

That’s it. Gust doesn’t offer an opinion on breakpoint management, dark mode / appearance handling, or custom pseudo selectors. Per gust’s readme:

Breakpoints, active/hover, color schemes, and more can be reasoned about in your codebase where you likely already have patterns, providers, or hooks for these problems. Because there is so much variety in the React Native ecosystem, the only way to create something broadly applicable is to keep it simple.

Under the hood, gust creates a higher order component, calculates (and caches) your flags, and then passes the final CSS off to tailwind-rn to generate your styles. The final result is Tailwind in React Native, the way you always imagined it.

Webmentions
What’s this?

Tweets, mentions, and trackbacksShare your thoughts

As this gets discussed, comments will show up here. If the post is new, it may take a bit for your thoughts to get from one side of the internet to the other.

Code Drift is the personal website of Rudolph Jakob Heuser