Tailwind

@ng-native/tailwind lets you write <view class="flex-1 bg-blue-500 p-4"> and have it work, because Fabric already has a real cascade - selectors, specificity, inheritance, media queries, custom properties - and class already matches against it. Nothing here is an interop layer translating className into style objects the way it would have to be on plain React Native: Tailwind's generated CSS goes through the same build-time CSS compiler your own component styles do, and the utility classes it produces are cascade rules like any other.

What this package adds is a build step that turns Tailwind's browser-flavoured output into the subset native can express, and a preset supplying the vocabulary Tailwind has no reason to ship on its own: platform variants, safe-area and hairline utilities, and touch-appropriate meanings for hover: and focus-visible:.

Setup

npm install @ng-native/tailwind tailwindcss @tailwindcss/cli
/* src/styles.css */
@import 'tailwindcss/theme.css';
@import 'tailwindcss/utilities.css';
@import '@ng-native/tailwind/native.css';

Import theme.css and utilities.css rather than the plain tailwindcss entry point, which would also pull in preflight - a browser reset written in terms of html, ::before and -webkit-*, none of which means anything on a phone.

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withAngularNative } = require('@ng-native/metro/config.cjs');
const { withTailwind } = require('@ng-native/tailwind/config.cjs');

module.exports = withTailwind(withAngularNative(getDefaultConfig(__dirname)), {
  input: './src/styles.css',
});

withTailwind runs @tailwindcss/cli against input once, synchronously, so the generated module exists before Metro resolves the first import. For a dev server it then leaves the CLI running in watch mode, so a class written in a template appears without restarting the server. For a one-off build (expo export, expo export:embed, react-native bundle, expo prebuild) and whenever the CI environment variable is set, it does not watch, and nothing it starts keeps the process from exiting when the build is done. watch: true or watch: false decides it either way. The output option - .angular-native/app.tailwind.js by default - is a .js/.cjs/.mjs module rather than CSS, because Expo's own transform worker claims every .css file before this package's transform is asked and hands back an empty module on any platform but web. A .d.ts beside it types the default export as the StyleSheet globalStyles takes. Both are rebuilt on every start, so .angular-native/ belongs in .gitignore - an app from the template ignores it already.

A fresh clone, or a CI job, has neither file until something loads the Metro config, so a typecheck run first fails with Cannot find module '../.angular-native/app.tailwind.js'. Loading the config builds them once and exits, without watching, so a typecheck script that does it first always has them:

"typecheck": "node metro.config.js && ngc -p tsconfig.json --noEmit"
// src/main.ts
import tailwind from '../.angular-native/app.tailwind.js';
import { mount } from '@ng-native/platform';

const app = mount(rootTag, App, fabric, { globalStyles: tailwind });

Pass the generated module as globalStyles, the one stylesheet Fabric matches against every node regardless of which component created it - which is what a utility class needs, since a class="p-4" can land on any element in the app.

What the build step does

Tailwind 4 emits CSS aimed at a browser: cascade layers, @property declarations, oklch() colours, and a spacing scale expressed with calc(). None of that is a cascade question - it never depends on what element it lands on - so it is all resolved once at build time, before the result ever reaches the same CSS compiler your own component styles go through. Whatever is left that native genuinely cannot express is reported on the line it was found:

[angular-native] app.tailwind.css:153: dropped 'appearance': 'appearance' is not mapped yet.

That is deliberate, not a rough edge - see what CSS reaches a device for the full shape of what this compiles to and what it drops.

Tailwind's truncate, line-clamp-*, line-clamp-none, whitespace-nowrap and text-ellipsis become a text's numberOfLines and ellipsizeMode, which is how native truncates, so put them on the <text> rather than on a view around it. tabular-nums and the other numeric variants become fontVariant.

The filter utilities follow the platform table on that page: brightness-* works on both platforms, and blur-*, grayscale, hue-rotate-*, drop-shadow-* and the rest are drawn on Android only, so unscoped they are dropped with a warning. Write them as android:grayscale to keep them for Android.

A unitless line-height, the ratio form CSS defines and the one Tailwind's leading-* utilities write, works with or without a font-size beside it. When the rule also sets a font-size, as every type-scale utility does, the build multiplies the two. Otherwise it is settled on device against the font size the element ends up with, the way an em is. One difference from the web: a descendant with a font size of its own inherits the resulting number of points, where a browser would apply the ratio to the descendant's size, so set leading-* on the text it is for.

Variants covers the platform, dark-mode, hover: and focus-visible: variants the preset adds and why native gives them different meanings than the web. Safe area and hairlines covers the two utility families this package adds that Tailwind has no reason to ship on its own.

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.