What CSS reaches a device

The rule of thumb throughout the whole compiler: if React Native's style API can express it, CSS has a spelling for it here; if it cannot, the declaration or rule is dropped with a build warning that names the file, the line and the reason, rather than shipping a screen that is silently missing a style. A dropped declaration leaves the rest of its rule in place. CSS that does not parse fails the build.

Selectors that work

Type, class and id selectors. Attribute selectors: [name], [name="value"], [name^="value"], [name$="value"], [name*="value"], [name~="value"] and [name|="value"]. :is(), :where() and :not() (each compound argument only - a combinator inside one of these is dropped, other than two forms Tailwind writes: <compound> *, the ancestor test its group-* variants compile to, and <compound> ~ * or <compound> + *, the sibling test its peer-* variants compile to, which is read as the sibling combinator it means). :host and :host(<compound>). :host-context(<compound>). :first-child, :last-child, :only-child, :nth-child() and :nth-last-child() (the of <selector> form is not supported). :empty. :root. :disabled (answered from the element's own disabled prop). :focus and :active, which the engine tracks itself from native focus, blur and press events. Every combinator CSS has works too: descendant ( ), child (>), next-sibling (+) and later-sibling (~).

html means the top of the tree, exactly as :root does: there is no element called html on native, and a stylesheet written for the web puts its tokens there.

Selectors that are dropped

A rule with one of these selectors is dropped with a warning.

::before, ::after and every other pseudo-element are permanently unsupported, not a "not yet". Rendering one would mean synthesising a node no template declared, which would make the style engine responsible for view hierarchy a template never wrote - so the fix, when a design wants one, is to write the element.

:hover and :focus-visible are also unsupported, because there is no hover or focus cascade to answer them from: a phone has no pointer to hover with, and focus arriving from a keyboard or an assistive technology looks identical to focus arriving any other way. Use :active and :focus, which the engine does track, or drive the state with a bound attribute such as [attr.data-hover]. The hover: and focus-visible: variants in @ng-native/tailwind already point at them for you.

:checked, :indeterminate, :valid, :invalid, :placeholder-shown and the other form-state pseudo-classes are unsupported too: a native control keeps that state in its component's inputs, where no selector can see it. Bind an attribute from the same signal ([attr.data-checked]) and select on that. :has() is unsupported, because a style that depends on what is beneath a node would have to be matched again whenever anything beneath it changed.

Everything else CSS can paint

Colours can be written as a named colour, hex, rgb(), hsl(), hwb(), lab(), lch(), oklab() or oklch(), with or without an alpha, and a color-mix() of two such colours in any of those spaces. Native paints sRGB only, so each is converted to rgb() at build time with CSS Color 4's formulas; a colour outside sRGB is brought inside by CSS Color 4's gamut mapping, which reduces its chroma rather than clipping each channel, so the hue holds. color() with a named space, such as display-p3, is dropped. A color-mix() with a var() in it is worked out on device once the token is known, in the space it names (srgb, oklab, oklch, lab, lch, hsl or hwb) with CSS Color 4's arithmetic: premultiplied alpha, a hue the shorter way round unless a hue method says otherwise, an achromatic colour's hue taken from the other colour, and the result brought inside sRGB by the same gamut mapping. That is what themes a list row by a bound custom property: [style.--cover]="track.colour" on the row, and color-mix(in oklch, var(--cover) 70%, white) for its lighter shade.

A relative colour from a token, such as oklch(from var(--brand) l c h / 0.2) or hsl(from var(--brand) h s calc(l - 20)), is worked out on device too, in rgb(), hsl(), hwb(), lab(), lch(), oklab() or oklch(). Each channel keyword is the origin's number in that space, on CSS Color 5's scale (r g b 0 to 255, the s l w b of hsl() and hwb() 0 to 100), and a channel is a keyword, a number, or calc() of them. A percentage or an angle stands alone as a channel, since CSS does not add one to a keyword. A hue a grey does not have is 0.

light-dark(<light>, <dark>) picks by the app's colour scheme, the one prefers-color-scheme reads; native has no per-element color-scheme. It works anywhere a colour does: a declaration, a token (--surface: light-dark(white, black)), a gradient stop, a shadow, inside a color-mix(), with tokens on either side.

Gradients (background-image: linear-gradient(...) or radial-gradient(...), compiled to the structure Fabric's experimental_backgroundImage prop reads) support linear and radial only, not conic. A stop can be a var(), a color-mix() with one in it, or a literal colour beside them; a stop that is a lone var() nobody defines is dropped, which is what makes an optional middle colour optional. A radial gradient with tokens in its stops takes its shape, size and centre as keywords and positions (circle closest-side at 50% 18%), not an explicit radius; a position can be a token too (at var(--x) 30%), with a fallback after its name. background-image takes gradients only - a url() in background-image is dropped, because there is no image loader behind that prop; put an image in an <image> element instead.

filter compiles to the list of functions React Native's filter prop takes, and which of them a device draws depends on the platform:

Function iOS Android
brightness(), opacity() yes yes
contrast(), grayscale(), hue-rotate(), invert(), saturate(), sepia() no yes
blur(), drop-shadow() no Android 12 (API 31) and later

On Android a list with blur() or drop-shadow() in it is drawn from Android 12 on and ignored whole before that; the others are drawn on every version. A function iOS does not draw is dropped with a warning naming the function in any rule that can apply on iOS: scope the rule to Android with a .platform-android ancestor, the class mount puts on the root (Tailwind's android: variant compiles to exactly that), and it is kept.

.platform-android .photo-disabled {
  filter: grayscale(1);
}

A component's stylesheet is compiled for the platform Metro is bundling, so an unscoped rule keeps its filter in the Android build and drops it with a warning in the iOS build; a sheet compiled with no platform, such as a component under a test, has to suit both. A Tailwind sheet serves both platforms too, so grayscale there is dropped with a warning, while android:grayscale is kept. A filter inside @keyframes is checked against the platform alone, since a keyframe has no selector to scope it.

transform takes the translate, scale, rotate, skew and perspective functions. The individual properties translate, rotate and scale, which are what Tailwind 4 writes, are properties of their own, as on the web: .rotate-45.translate-x-4 both turns and moves, and each can be transitioned (transition: rotate 200ms) or animated in @keyframes separately. They apply in the order CSS gives them, translate, then rotate, then scale, then transform. transition: transform does not animate them, because they are not transform; name them, as Tailwind's transition-transform does. rotate turns about x, y or z; any other axis is dropped. Any of them can take tokens, translate: var(--tw-translate-x) var(--tw-translate-y) and transform: translateY(var(--y)) rotate(var(--r)) alike, with calc() around them; an angle token is read in degrees whatever unit it was written in, so --r: 0.25turn turns 90. With a token in it, transform takes the translate, scale, rotate and skew functions.

box-shadow and text-shadow both work, though native has room for exactly one text-shadow, not a list. On iOS a text-shadow is drawn inside the text's own box, so a blur or offset that reaches past it is cut off square; give the text padding as deep as the shadow. A box-shadow can have tokens in it as design systems write them: a colour that is a var(), a color-mix(), a relative colour, rgba(var(--channels), 0.25) or an hsl() of tokens; a length that is a var() or calc() around one (0 0 0 var(--ring-width)); and a whole shadow that is a token, in a list with others, with the shadows after its name as the fallback. A shadow whose lengths are all one token is the exception, since a token is read in one form: write its lengths out, or put the whole shadow in the token. A text-shadow takes tokens the same way, in its colour and its lengths.

Truncation is a paragraph's props on native, not a style, and CSS's three ways of asking for it compile to them: white-space: nowrap to numberOfLines: 1, line-clamp or -webkit-line-clamp to that many lines, and text-overflow to ellipsizeMode (ellipsis is tail, the default, and clip is drawn on iOS only). line-clamp: none, unset and white-space: normal clear the limit. They apply to the <text> the rule matches, where the props are read: on a <view> around the text they do nothing, so put the class on the text itself. display: -webkit-box is read as the flex box native already is, laid out along the axis -webkit-box-orient names, so the whole line-clamp idiom compiles; -webkit-box-orient without it is ignored, as a browser ignores it. A [numberOfLines] bound on the element wins over the stylesheet. The other white-space values, which keep spaces and line breaks, are dropped. font-variant-numeric takes tabular-nums, proportional-nums, lining-nums and oldstyle-nums, as fontVariant.

display takes flex, none, block and contents. Every native view is already a flex container stacking its children in a column, which is what a block does with its own, so block is read as flex: that is what lets .d-none followed by .d-md-block show an element again at a breakpoint. inline-flex is read as flex for the same reason. contents is Yoga's own: the element draws no box of its own (no background, border or padding) and its children are laid out as if they were its parent's. inline, inline-block, grid and the table values are dropped, because a column of flex children cannot pretend to flow them side by side.

overflow is one value for both axes - Yoga has no separate overflow-x/overflow-y - so a rule that gives them different values is dropped with a warning rather than silently picking one.

The logical properties all work: inset-inline, inset-block, margin-inline, margin-block, padding-inline and padding-block, with their -start and -end longhands, and the border shorthands border-inline, border-block and their -start and -end sides, so Tailwind's inset-x-*, inset-y-*, mx-*, ps-*, start-* and the rest do what they say. The block axis is always top to bottom, since native has one writing mode. The inline axis follows the layout direction: -start and -end are Yoga's start and end edges, which swap in a right-to-left layout, and a pair written with one value (inset-inline: 0) is simply both sides. They compile to the edges every native view reads, not to React Native's own insetInlineStart and marginBlock spellings: those are aliases that some views, <safe-area-view> among them, never apply, so a class on one did nothing. One difference from the browser: where a start or end edge meets a physical one (inset-inline-start and left on the same element), the start or end edge wins, whichever was written last.

border-style is likewise one value for all four sides: a native border has one style, and a per-side style that disagrees with the others is dropped for the same reason. border-top and the other per-side shorthands set that side's width and colour, and take solid (native's default) or none as their style.

A custom property can hold a font stack (read as its first family, as font-family is), a unitless line-height, a ratio for aspect-ratio, a whole box-shadow list, or bare colour channels for rgba(var(--channels), <alpha>), which is how Bootstrap writes its colour utilities. A shorthand may mix var()s and written values: padding: var(--y) var(--x), border: var(--width) solid var(--colour). flex: var(--grow) is flex: <number>: it grows by the token, shrinks by 1, and starts from a basis of 0.

calc() may add one viewport or font-relative length to absolute ones (calc(1.375rem + 1.5vw)), which is settled on device. min(), max() and clamp() fold when every argument is absolute. A calc() may have any number of tokens in it, with numbers and absolute lengths, + - * / and brackets: calc((var(--end) - var(--start)) * 1px) is worked out on device, where a token that is a length counts in points and a bare number multiplies. A percentage, an em or a viewport unit beside a token is dropped, since it needs layout the device does not do there.

The keywords that switch a property off - max-width: none, z-index: auto, letter-spacing: normal, filter: none, box-shadow: none - clear it back to native's default. The CSS-wide keywords (inherit, initial, unset, revert, revert-layer) are dropped, and the warning says so.

Layout is Yoga's, which follows CSS flexbox with one difference worth knowing: an absolutely positioned child's percentage size is taken from the width its parent was offered, not the width the parent shrinks to. A child at width: 84% inside a parent sized by align-self: flex-start is 84% of the space around the parent. Give a parent like that an explicit size, as a star rating's row of fixed-width stars has.

Transitions, animate.enter/animate.leave, @keyframes and animation are their own page: see Animation.

What has nothing to map onto

These are dropped with a warning that says so, not declarations that quietly do nothing:

  • float
  • grid and its whole family
  • list-style
  • Table layout: table-layout, border-collapse, caption-side, empty-cells
  • Multi-column layout
  • will-change
  • contain
  • CSS counters
  • clip-path

What a warning says

Each warning names the file and line, the component, what was dropped and why, and the native alternative where there is one:

[angular-native] src/app/card.ts:12 (Card): dropped 'float': 'float' has no React Native equivalent: no style prop of a native view does what it does. Lay the row out with flexbox: flex-direction: row on the parent.

The rest of the rule still applies. A rule whose selector native cannot match is dropped whole, with a warning that says dropped a rule and why.

A stylesheet lifted from a design system builds as it is, with a warning for each web-only declaration in it. See Metro.

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.