Variants

@ng-native/tailwind ships three stylesheets. shared.css holds the parts that do not depend on the platform; native.css and web.css each import it and add their own answers for the variants where a touchscreen and a browser genuinely disagree. Import native.css in an Expo app and web.css in a browser build.

Platform, disabled and dark variants

Tailwind cannot infer what platform it is running on, so the variants match a class on the root: platform-ios, platform-android or platform-web. mount puts it there itself - on a phone from nativePlatform(), and platform-web from @ng-native/web's mount - so ios:, android:, web: and native: (either native platform) work with nothing to set up. A variant whose class is never present simply never matches rather than erroring - ios:pt-2 in a shared class string is not a mistake on a web page, it is just not an iPhone.

Variants stack: android:dark:bg-zinc-950 applies on Android in dark mode, whether the two classes sit on one ancestor, as they do on the root, or on two.

disabled: matches [data-disabled] as well as :disabled, because a control's disabled input is consumed by the behaviour composed onto it and never left as a prop :disabled could read; data-disabled is what that behaviour publishes instead.

dark: matches a .dark class. watchConditions(app.engine) - which the template's src/main.ts already calls - keeps dark on the root in step with the system scheme, so dark: follows the device with nothing to set up. An app with its own theme switch passes { darkClass: false } and puts dark on its own root view instead:

watchConditions(app.engine, { darkClass: false });
<view [class]="theme()" style="flex: 1"></view>

Tailwind's own dark: is @media (prefers-color-scheme: dark), which the CSS engine already answers and keeps in sync with the system. A class is one step further: it lets a theme switcher disagree with the OS, which prefers-color-scheme on its own cannot do.

This is also why a palette defined as custom properties keeps working. The build step inlines a var() wherever it can, since there is no CSS parser on the device to resolve one later. A custom property declared more than once with different values - --primary under :root and again under .dark, which is exactly how a theme is written - is the exception: which declaration wins is a question only the runtime cascade can answer, so those survive the build untouched and are resolved per node. Define your tokens that way and the switch reaches them.

hover: and press:

On native, hover: means the pressed state: :hover is dropped with a build warning there and always will be - there is no pointer cascade to answer it - but a hover style and a press style ask the same design question. The native preset points the variant at :active (set by the engine while a touch is down) and also data-hover, for the one native device with a real pointer - an iPad with a trackpad, which React Native's pointer events do report. press: and hovered: name one half each, for a component that wants to be precise rather than portable.

On the web, hover: keeps the real :hover as well as :active and data-hover, so a class string written once behaves the same on a desktop browser, a touchscreen laptop, and a phone browser without the component knowing which it is on.

/* native.css */
@custom-variant hover (&:active, &[data-hover]);

/* web.css */
@custom-variant hover (&:hover, &:active, &[data-hover]);

focus-visible: and focus:

On native, focus-visible: is aliased straight to focus:, because the distinction it exists for on the web - keeping a ring off a control someone clicked - has no case on a phone: focus only ever arrives from a keyboard, a remote or an assistive technology, exactly the situations a ring is for. :focus-visible is dropped with a build warning on native for the same reason :hover is. Both platforms also match [data-focus], because focus lands on the control itself while the border and padding that form the ring usually sit on a wrapper around it, which can only know it is focused because the behaviour composed onto the control told it.

peer-* and group-*

group-* variants match an ancestor with the group class, and peer-* variants match an earlier sibling with the peer class, exactly as on the web. The state a variant reads has to be one the engine can see on the peer: peer-focus: (focus, or data-focus), peer-active: and peer-hover: (a press), peer-disabled: (a disabled prop, or the data-disabled a text input publishes), peer-data-[...]:, peer-aria-[...]: on an attribute the peer really carries, and the arbitrary peer-[.is-on]:. A class or a state arriving on the peer restyles the siblings after it.

<text-input class="peer border-b-hairline" placeholder="Email" />
<text class="text-gray-500 peer-focus:text-green-600">We never share it</text>

peer-checked: and group-checked: are dropped at build time with a warning that says why: a switch keeps whether it is checked in its component's checked input, where no selector can read it. Publish it as an attribute and use the data variant instead:

<switch class="peer" [(checked)]="on" [attr.data-checked]="on() ? '' : null" />
<text class="peer-data-checked:text-green-600">On</text>

font-mono

Tailwind's --font-mono is a browser font stack starting ui-monospace, and native's fontFamily takes a single name, not a fallback list - the compiler keeps only the first family from any stack, silently. ui-monospace exists on neither iOS nor Android, so font-mono would silently fall back to the system font everywhere without help. native.css sets --font-mono: 'Courier New' (a real font on both, so it means something even with no platform class on the root) and then upgrades it per platform - Menlo on iOS, the monospace alias (Roboto Mono) on Android - once .platform-ios/.platform-android is present. web.css does not need this: it does not import it, and a browser resolves ui-monospace on its own.

The same silent first-family-only rule applies to any font stack you write yourself, Tailwind's or not: give native a single, real family, the way native.css does for font-mono.

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.