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(), of compounds or of any selector, :is(.a + .b) and :not(.list > *) among them,
each as specific as in a browser. :is() and :where() read three forms Tailwind writes as a test
of one compound: <compound> *, the ancestor test its group-* variants compile to,
<compound> > *, the parent test its *: variant compiles 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).
:first-of-type, :last-of-type, :only-of-type, :nth-of-type() and :nth-last-of-type(),
which count the siblings with the element's own name. :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 (~).
::ng-deep is read where something is written before it: :host ::ng-deep .inner and
.panel ::ng-deep .inner style .inner anywhere under that host, or under that element of the
component, in the views of the components it holds as well. Written first, with nothing before
it, it is a rule for the whole app in a component's sheet, and is dropped with a warning: put
such a rule in the global sheet.
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.
In an app's own sheet, ::before, ::after and every other pseudo-element are unsupported, not a
"not yet". Rendering one would mean synthesizing 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. A
sheet read as a browser reads it, for components written for
one, is the exception for ::before and ::after.
::placeholder is the exception, because it needs no node: a text input draws its own placeholder,
and its placeholderTextColor is the colour. .field::placeholder { color: #8b8b96 }, or
Tailwind's placeholder:text-gray-400, sets that colour on a <text-input>, a token in it
included, and on an element named input or textarea where an app or a package has registered one
as a text field. An opacity in the rule fades that colour, since native has no opacity for a
placeholder: opacity: 0 hides it and opacity: 1 leaves the colour as it is. With no colour in
the rule the placeholder is the platform's default, and with platform-color() it is the platform's
to work out: opacity: 0 hides either and any other value leaves it alone. A placeholder takes only
a colour and an opacity, so anything else in the rule is dropped with a warning, and the rule
matches those three elements only, as a browser's matches an input.
: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. A sheet read as a browser reads it reads
:checked, :indeterminate and :enabled from the element's own props.
:has() styles a node by what is beneath it, or after it. Its argument is any relative
selector: a descendant (.card:has(.action)), a child (.card:has(> img)), a longer selector
(.card:has(.row > .badge)), or what follows the node (.label:has(+ .input),
.item:has(~ .panel .mark)), and a list of any of these. It is read on the node the rule styles,
on a box that node is in, and on one before either: .card:has(.action) .title styles the titles
in a card that has an action, .field:has(.input:focus) > .label a field's own label, and
.card:has(.action) + .title the title after such a card. A :has() inside another
(:has(.a:has(.b))) is dropped with a warning, as a browser drops it as invalid. Tailwind's
has-[...], has-data-[...], group-has-* and peer-has-* variants are supported.
A sheet that uses :has() has the engine match a changed node's ancestors again on each change,
and restyle one only where the rules it matches came out different. Where a rule asks it of a box
the styled node is in, the nodes under that box that such a rule is for are matched again too,
once for a commit, and where it asks what follows a node, the elements before a change are
matched again too. A sheet that does not use it costs nothing.
Everything else CSS can paint
Colors can be written as a named color, hex, rgb(), hsl(), hwb(), lab(), lch(),
oklab() or oklch(), with or without an alpha, and a color-mix() of two such colors 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 color 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 color's hue taken from the other color, 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 color 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 gray does not have is 0.
light-dark(<light>, <dark>) picks by the app's color scheme, the one prefers-color-scheme
reads; native has no per-element color-scheme. It works anywhere a color 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 color beside them;
a stop that is a lone var() nobody defines is dropped, which is what makes an optional middle
color optional. A radial gradient with tokens in its stops takes its shape, size and center 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. background-image: var(--g) draws
the gradients its token holds, in a stylesheet's token, one set on an element or a fallback. A
token of none, one with a color in it, linear-gradient(...), red, or one that holds no image
leaves no image, as in a browser, where such a value is invalid.
The background shorthand takes the same gradients, one or several, with a position, size,
repeat and color beside them: background: linear-gradient(red, blue) center / cover no-repeat.
What it leaves out is back at its initial value, as in a browser: background: red or
background: platform-color(link) takes away a gradient an earlier rule gave, and
background: linear-gradient(red, blue) a color. A shorthand
that is a token, background: var(--surface), is the exception described below.
background-clip: text is not a value any native view takes. It marks its rule as one for
<gradient-text>, the element that draws a background
through its letters: the rule applies to that element alone, and one written for another element
is dropped with a warning. The other values of background-clip are dropped.
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.
A filter can also be a list of tokens, each holding one function or nothing: filter: var(--blur,) var(--grayscale,) draws whichever of them are set on the element, from whichever
rules set them. Each token is checked against the platforms of the rule that sets it, so
--grayscale: grayscale(1) in a rule that can apply on iOS is dropped with the same warning.
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.
In a [style] binding, write a transform as a string, { transform: 'rotate(45deg)' }. React
Native's array form, { transform: [{ rotate: '45deg' }] }, renders the same, but Angular's dev
mode checks each style value and logs NG0318 for one that is not a string or a number, every time
the binding changes. For a transform that follows a signal frame by frame, that is a warning a
frame.
A style attribute or a [style] binding has no compiler to read it, so it takes fewer forms of
some shorthands than a stylesheet. border, its sides and logical sides, outline, box-shadow,
background, background-image, font and text-decoration are read into the same props a
stylesheet gives them. A line takes a token beside written parts, border: 1px solid var(--c),
and a background takes one as all of it or as its last layer. In these, background is a color
or linear gradients, with no position, size, repeat or url(), and no token in a gradient, and
background-image is linear gradients or none, or a token as all of it. box-shadow takes a
token only as all of it, lengths in px or rem only, and a color written out, not a
color-mix(). font has no small-caps, stretch, keyword size or system font.
A value outside that is dropped, with a warning in development.
skewX() and skewY() are drawn on iOS only. React Native on Android breaks a transform down into
the rotation, scale and translation an Android view has, and a view has no skew, so skewX() is
left out and skewY() comes out as a rotation. A skew is refused the way a filter iOS does not draw
is, with the platforms the other way round: it is dropped with a warning in any rule that can apply
on Android, and kept in a rule scoped with a .platform-ios ancestor (Tailwind's ios: variant),
in an iOS build, and in a keyframe of one. A skew of 0 is kept everywhere, since it draws the same
on Android.
box-shadow and text-shadow both work, though native has room for exactly one text-shadow, not
a list. none switches either off, over a weaker rule's shadow or, for a text-shadow, an
inherited one. 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 color that is a var(), a color-mix(), a
relative color, 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 shadow token can have a token in its color too, --ring: 0 0 0 2px var(--ring-color, currentcolor), filled in where it is used. currentcolor as the fallback of a shadow's color is
the element's color, or the one it inherits, or black. A text-shadow takes tokens the same way,
in its color and its lengths. One with no color is inherited as currentColor, so a text inside it
with a color of its own draws the shadow in that color.
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.
Text that is one word, with no space or hyphen to break at, is one line whatever its box, cut at
the edge where no text-overflow says otherwise: a browser does not break a word inside, and
native breaks one between any two letters. A line-clamp or a [numberOfLines] of its own wins.
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.
Where wrapped text breaks is a paragraph's props too, and these are inherited, as in CSS, so a
class on a <view> reaches the text inside it. text-wrap: pretty (or text-wrap-style: pretty,
Tailwind's text-pretty) is iOS's lineBreakStrategyIOS: 'standard', the strategy UILabel and
SwiftUI's Text use, which keeps a short word off a last line of its own; without it iOS breaks as
React Native does, with none. text-wrap: balance is Android's textBreakStrategy: 'balanced',
which evens the lines out; iOS has no balanced breaking and draws it as none. wrap, auto and
stable are each platform's default, and text-wrap: nowrap is dropped: write white-space: nowrap. word-break: keep-all (Tailwind's break-keep) is iOS's hangul-word, which keeps a
Korean word whole, and word-break: normal is none; as both set lineBreakStrategyIOS, the later
of a text-wrap and a word-break declaration wins, where CSS keeps both. The other word-break
values are dropped. A lineBreakStrategyIOS or textBreakStrategy bound on a <text> wins over
the stylesheet.
font-variant-numeric takes tabular-nums,
proportional-nums, lining-nums and oldstyle-nums, as fontVariant.
display takes flex, none, contents, the block containers block, flow-root and
inline-block, inline-flex, and grid and inline-grid. An element that is display: none has
no native view, nor has anything inside it: its views are made when it is displayed and go when it
is not, so what a view holds of its own, a scroll offset, is not kept across a spell of display: none. To keep a view while it is out of sight, take it out of the flow and give it no opacity. Yoga
lays out flex boxes and nothing else, so a block container is committed as a flex column, which
stacks its children as a block does, and the engine settles what a column does differently before
layout:
- Adjoining vertical margins collapse, as CSS has them: between two children of a block, through
an empty block, and between a block and its first or last child where the block is itself a
child of a block and no padding or border separates them. A margin of
20pxabove one of10pxleaves 20, where a flex column leaves 30. - The flex properties on a block container (
flex-direction,justify-content,align-items,gap) and on the boxes in its flow (flex,flex-grow,flex-shrink,flex-basis,align-self) are ignored, as a browser ignores them.justify-selfplaces a block of a set width along its line, andmargin: 0 autocentres one. - An
inline-block,inline-flexorinline-gridbox sits in a line. Where a block container holds nothing else in its flow, they are laid out in lines, a row that wraps, each on the baseline of its line. Among blocks, each run of them shares a line of its own, wrapping when it is full: each box as wide as its content, with margins that do not collapse. The engine commits a view around a run of two or more, which no element of the template stands for; a box that joins such a run or leaves it is created again natively, so a scroll position or a focus inside it is lost.vertical-aligntakestop,middleandbottomfor a box on a line, and a length, how far over the baseline it sits.
A box in a line is as wide as its content, and that includes content Yoga would size from the
room on offer: a row with a percentage width, as a w-full header is, and cells that share a row
from a basis of nothing, as flex-1 cells do. The engine commits such a box with those set aside,
reads its width back, and commits it at that width, which the percentage and the shares are then
of. It costs the box two more commits when something in it changes.
A row that is as wide as its content, such as one with align-self: flex-start in a column, an
item of another row, or one that is width: fit-content or max-width: max-content, is as wide as
its items when one of them grows, as in a browser: a tray of tabs that are each flex: 1 hugs the
tabs. Yoga, as React Native configures it, gives such a row all the room it could have once any
item has a flex-grow or a React Native flex above 0, and with layoutConformance: 'strict' it
gives the row no width when the item grows from a basis of zero points, such as flex: 1 1 0px.
The engine fits the row the same way, with the items' growth set aside, at the same cost. Once the
row is stretched, by align-self: stretch or a width, the items share all of it.
Only margins in points collapse. Beside a percentage margin in the same container every margin stays
as written, because a percentage is of a width nothing knows before layout. In a flex container or a
grid an inline-level box is an item like any other, as a browser has it. .d-none followed by
.d-md-block shows an element again at a breakpoint. inline is read as flex, because every
native view is a box, and a browser lays out a flex container's inline children as blocks.
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. The table values are dropped. display: var(--d) reads the token on device the same way, in a stylesheet or set on the element, and reads
the two-keyword form, inline flex or block flow, as the one-word value it stands for. A token of
any other value, such as a table value, unsets display, and the element is laid out as a flex
column. In development the engine logs a warning naming the token and its value, since the compiler
only sees the token's name. A written fallback is read the same way, var(--d, inline flex)
included. One native has no layout for, such as var(--d, table), is dropped with a build warning,
and the token is still read where it is set.
Grid
display: grid is laid out by the engine, since Yoga has no grid, and so is what an inline-grid
holds. It follows CSS Grid Level 1:
grid-template-columnsandgrid-template-rowstake lengths, percentages,fr,auto,min-content,max-content,minmax(),fit-content()andrepeat(), withauto-fillandauto-fit, which repeat only tracks with a length on one side at least, as in a browser, and each a point wide at the least.grid-auto-columns,grid-auto-rowsandgrid-auto-flowsize and fill the tracks the template does not name.grid-column,grid-row, their-startand-end, andgrid-areaplace an item by line number, byspan, or by name. An item with no placement takes the next free cell.- A
position: absolutechild of a grid that is itself positioned is placed against the lines itsgrid-columnandgrid-rowname: its insets are from the edges of that area and its percentages are of it. A side with no line, orauto, is the grid's padding edge. With no inset on an axis it sits at the start of the area, moved along it by itsjustify-selforalign-self, or by the grid'sjustify-itemsoralign-itemswhere its own isauto. grid-template-areasnames areas, whichgrid-area: headplaces an item in, and whose edges are the lineshead-startandhead-end. A track list names its own lines,[content-start] 1fr [content-end], andgrid-column: contentis the two of them. A name given to several lines is counted:col 2.gap,justify-content,align-content,justify-items,align-items,justify-self,align-self,place-itemsandplace-selfspace and align,safeincluded.
A grid needs to read sizes back from native to place its items: its own width, the width each item's content wants, and each item's height at its column's width. It commits, reads, and commits again, in the one task, so only the placed grid is drawn. That costs a few times what a flex container costs, and a grid changed inside is laid out again. Some limits follow from it:
- A grid with no width of its own, and none from its container, is as wide as its tracks come to, up to the room it is in: a centred dialog whose content is a grid. Its items are measured at the width that room allows.
- The narrowest an item's content can be is read where a track asks for it, a
min-contentorfit-content()track or an item that is not stretched: its text is committed one word to a line, and the widest is that width. A script that breaks anywhere, or a hyphenated word, is measured as the whole word. - Text written straight into a grid is not an item. Put it in a
<text>. subgrid, thegridandgrid-templateshorthands, and a span counted in named lines,span col 2, are dropped with a warning.align-items: baselineandalign-self: baselineline up the items that start in one row by their first baselines, read from native.last baselineis read asstart, and an item that is itself a grid has its bottom edge for a baseline.- An item with an
aspect-ratioand no height is as tall as its width makes it, where an item without one is stretched over its row.
On a host with no layout to read, such as a test's fake Fabric, a grid is a flex column.
Order
order moves a child of a flex container among its siblings, as in a browser: Tailwind's
order-first, order-last and order-2. Yoga has no such property, so the engine commits the
children in that order. A block lays its children out as written, whatever their order.
Position
position takes relative, absolute and static, which Yoga lays out, and fixed and
sticky, which the engine does.
A position: fixed box is positioned against the page it is on: the screen of a native stack or
a modal it is inside, and otherwise the whole app. Its insets and percentage sizes are of the page,
wherever in the page it is written. It stays where it is written in every other way, so it draws
in the order it is written in and takes an ancestor's opacity, as in a browser. The engine reads
the page's place back from native to work its insets out, as it does a grid's sizes.
Where something between a fixed box and its page scrolls or clips, a scroll view or an
overflow: hidden box, the fixed box is committed under the page instead, so it neither scrolls
nor is clipped. So is a fixed box as wide as its content, with no width of its own and not stretched
between a left and a right, so that its text wraps in the page and not in a narrower box it is
written in. On iOS a fixed box goes back under its parent once it has a width of its own or is
stretched between a left and a right, and nothing it is in scrolls or clips any more. On
Android every fixed box is committed under its page.
A fixed box committed under its page keeps its static position on an axis it has no inset on, as in a browser: where it would be as the only child of the box it is written in, inside that box's padding and border where those are in points, and at its start, centre or end as that box aligns its children, with the start on the right in a box laid out right to left. Its text then wraps in the room between there and the far side of the page. A centred one is centred by its own size. One wider than twice the room on the nearer side of its centre starts that far from its centre, as in a browser, but stays as wide as its text, where a browser wraps the text to that width. Inside a scroll view it stays at the page's start on that axis instead, since native reads back where a box in a scroll view has scrolled to.
Under its page a fixed box draws over the page's other content, and an ancestor's opacity or
transform no longer reaches it; it is still styled by rules that match through its parent, its
events still bubble to its parent, and it is not shown while anything it is inside is
display: none. A transformed ancestor does not become what a fixed box is fixed to, as one does
in a browser.
Two things Yoga leaves undone for a position: absolute or fixed box are put right from its
laid-out size, read back from native as a grid's sizes are. margin: auto between two insets
takes the room left over, so inset: 0; margin: auto centres a box of a set size in its
container, and one auto margin pushes it to the far side. And a box with no top or bottom in
a block container sits under the sibling before it, where it would have been in the flow. In a
flex container such a box is placed by the container's alignment, as Yoga and a browser both do.
A position: sticky box is laid out where it is written, and moves with its scroll view once the
scroll passes it: top is how far from the scroll view's top edge it sticks, and it goes no
further than the end of its parent. In a horizontal scroll view left does the same. It sticks
at any depth inside the scroll view, and native moves it on each frame the scroll view moves.
Sticking to the bottom or the right edge is not supported, and neither is a box with a
transform of its own that also sticks, whose transform the engine writes. It needs layout read
back from native, so on a host with none it stays where it is written.
width: fit-content and height: fit-content keep a box at the size of its content. Along its
container's main axis a box is that size already. Across it, where a box stretches, it stops
stretching and sits at the start, as in a browser, unless the container's align-items or its own
align-self already places it. max-content is read as fit-content on a width or a height,
and on max-width or max-height caps a box that would fill its container at its content. Under
a percentage the box is the smaller of the two. A box in the flow that is width: fit-content and
holds a percentage width or an item that grows is no narrower than what in it cannot get
narrower, a box of a set width say, and overflows its container as in a browser. One between two
insets, or one holding neither, is held to the room it has.
A box out of the flow that is as wide as its content by width: fit-content or a cap stays so
between a left and a right, where Yoga would stretch it: the engine fits it within the room
they leave, reading its width back, at the cost of two more commits when something in it changes.
A flex-basis of a percentage in such a box, or in any box that is fitted, is a share of the
width it comes to, as in a browser. While that width is worked out, the item counts for its own
width where that is a length, and for its content where it has none or its width is a
percentage, which is of the width being worked out.
Items with a basis of nothing (0% or 0) in such a box, tabs that are each flex-1 in a
w-fit row, share its width as in a browser: aa and bb cc dd come out the same width, the
second on two lines. A tab whose share would be narrower than its longest word is held at that
word, as a browser holds an item with min-width: auto, and the others share what is left, so no
word is broken; a browser under the layout reset lets the word overflow the tab. One with a 0%
basis that does not grow is held at its longest word too; one with a basis of 0 points that does
not grow stays 0 wide, as in a browser. A tab with a min-width of its own keeps that minimum
instead, and none is held wider than the row.
min-content is dropped with a warning, as is fit-content on min-width, max-width,
min-height and max-height.
min-width: auto and min-height: auto keep a flex item that shrinks from shrinking under its
content, as a browser keeps every flex item: Tailwind's min-w-auto and min-h-auto. Yoga's
minimum is nothing, so the engine stops the item shrinking where its content cannot get smaller:
down a column, and along a row when no text in it has a space to wrap at. An item holding text
that wraps still shrinks, under its longest word if the row is that crowded, and so does one with
a width, a flex-basis, a flex or an aspect-ratio of its own. An item with overflow other
than visible shrinks as it does in a browser. A native view has no minimum unless it is given
one, so nothing changes for an item that does not say auto, other than an item with a basis of
nothing in a box fitted to its content, as above.
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, and
so is overflow-x or overflow-y on its own, outside a
sheet read as a browser reads it.
overflow: auto is read as scroll, which is how Yoga lays out a scroll container, and clip as
hidden. cursor takes auto and pointer, the two a pointer on an iPad draws, with default
read as auto; any other cursor is dropped with a warning. So is a mix-blend-mode native does not
draw, plus-darker, and a text-decoration-line of overline, which native has no line for. A
text decoration is drawn under the text inside the element that declares it, in that element's
text-decoration-color or its text colour, as on the web. Android draws every line in the colour
of the text it underlines, because its text has no decoration colour.
pointer-events is inherited, as on the web, and none is about the element alone: it and whatever
inherits the value take no touches, and a descendant that sets pointer-events: auto takes them
again. An element with such a descendant is committed as React Native's box-none, and one with
none as none. React Native's own box-none and box-only are taken as written and are not
inherited. pointer-events: inherit and unset are the parent's value. The pointerEvents prop
keeps React Native's meaning, where none is the whole subtree. On Android, text and images take no
pointerEvents of their own, so text written directly inside a box-none element still takes
touches there; put it in a view to keep them off.
touch-action is read for a touch that starts on an element whose touch-action keeps a sideways
drag for itself (none, or pan-y with no pan-x), or on anything inside one. Until the finger
lifts it holds the screen's swipe back off, on iOS 26 and later, and keeps the drag from a
<scroll-view> the element is in: every drag with none, and one that sets off sideways with
pan-y. See Screens. No view is given it, and it does not decide which
view takes a touch. A keyword that stands alone (auto, none, manipulation) written beside
another is dropped with a warning. Tailwind's touch-none, touch-auto and touch-manipulation
are read; touch-pan-x and the other utilities Tailwind composes from tokens are dropped with a
warning.
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. Where a start or end edge meets the physical one it lands on
(inset-inline-start and left on the same element, left to right), the later or stronger
declaration wins, as in a browser.
React Native's own axis shorthands, which a binding sets as [style.paddingHorizontal], have a
spelling here too: padding-horizontal, padding-vertical, margin-horizontal and
margin-vertical. Each takes one value, and compiles exactly as padding-inline, padding-block,
margin-inline or margin-block with that value does: to both edges of its axis. So the cascade
settles it against the edges as it settles any shorthand, where React Native would always prefer
the edge: a later or stronger padding-left or padding-inline-start overrides it on that edge,
and it overrides an earlier or weaker one. A second value is dropped with a warning; write
padding-inline for a different value on each side. A browser has none of these properties, so
the web host ignores them: a sheet that also runs there writes padding-inline
and the rest.
border-style is likewise one value for all four sides: a native border has one style, so the sides
that are drawn are all drawn in it, and a second drawn style on one side is not one native can give
it. Which sides are drawn at all is per side: none on one side is supported. border-top and the
other per-side shorthands set that side's width and color, and take solid (native's default) or
none as their style. Any other style there, as in border-top: 1px dashed red, is drawn solid
with its width and color, and the build says so. A side styled none, by its shorthand or by
border-right-style and the other three, has no width whatever width another rule gives it, as CSS
computes it, until a rule styles that side again. A border with no color is drawn in the element's
text color, its own or inherited, as on the web: border: 2px solid, a border-width with no
border-color, and a side with a width that no rule gave a color. So is a border color of
currentColor, which is what Tailwind's border-current writes. Where no text color is set
either, the border is black. An outline, a box-shadow or a text-shadow with no
color, an outline-color, a background-color and a text-decoration-color of currentColor, a
var() that falls back to currentColor and a custom property that holds it take the text color
the same way, where they are used. color: currentColor and color: inherit are the color the
element inherits, as on the web, in a rule or in a @keyframes frame. All of them follow the text
color when it changes. A border shorthand's width may be a calc() of one token, as in Bootstrap's
border-top: calc(var(--bs-border-width) * 2) solid, and its color a color-mix() of a token, as
in border: 1px solid color-mix(in srgb, var(--tint) 35%, transparent); with the token unset there
is no border, as on the web. currentColor as a side of such a mix, written or held by a token, is
the text color where the mix is used. A border-width with no color anywhere is drawn black,
native's default, and the web host draws it black too.
A custom property can hold a platform colour, --accent: platform-color(link, "?attr/colorAccent"),
or a system colour such as LinkText, in any sheet, in a @media rule such as
prefers-color-scheme: dark, or bound with [style.--accent]. var(--accent) in a colour
property (color, background-color, a border color, outline-color, text-decoration-color,
tint-color, shadow-color or a text-shadow's color) commits what the colour written there
commits, and so does a var() that falls back to one. The platform resolves such a colour by name,
so it has no channels: a color-mix(), a relative colour, a gradient or a box-shadow worked out
of the token, or a fade of it such as Tailwind's bg-accent/50, is dropped. The build says so where
the sheet that sets the token also does that, and in development the engine says so on the device,
naming the token, wherever it is set. A colour property in an element's own style,
style="color: platform-color(link)" or [style.color], takes platform-color() written out as a
stylesheet does. In a stylesheet, the shorthands whose colour is a property of its own take
platform-color() or a system colour written out too, and as a var() fallback: text-shadow,
border and its sides, outline and text-decoration, as in
border: 1px solid platform-color(link). A box-shadow drops one, and the build says so.
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 color channels for
rgba(var(--channels), <alpha>), which is how Bootstrap writes its color utilities. A shorthand may
mix var()s and written values: padding: var(--y) var(--x),
border: var(--width) solid var(--colour). A token of two to four lengths gives a shorthand of
four sides or corners a value each, as a component library's design tokens do:
padding: var(--list-padding) of --list-padding: 0.25rem 0.5rem, and the same for margin,
border-width and border-radius. A part of such a token can be a token itself,
--content-padding: 0 var(--modal-padding) var(--modal-padding), but not a calc(). Any other
length property is unset by such a token, gap and padding-inline among them.
background: var(--surface) is read as
background-color, the one part of that shorthand native has, and so are
rgba(var(--channels), <alpha>), color-mix(), light-dark() and a relative colour of a token
written there. The token is read as the whole shorthand, as a browser reads it: gradients over a
colour, linear-gradient(...), red, or a layer with no image, center / cover or
rgb(1 2 3) none, in a stylesheet's token or one set on an element. A gradient placed anywhere but
the initial position, size and repeat, or a url(), is not read, and unsets the shorthand. With
anything written beside the token, background: var(--surface) none, the shorthand is refused.
Layers written out over a token's colour,
background: linear-gradient(#ffffff33, #ffffff00), var(--primary), are read as the shorthand
with the colour written, and the token as its background-color, as long as the token is all
of the last layer and no other layer has one. A token that is unset, or a word that is no part of a
background, makes the whole shorthand invalid, as in a browser, so the gradients go with the
colour. A token or a fallback that is a valid last layer with no image, a length, two lengths,
none, or a position, size, repeat, attachment and box with a colour or without, keeps the
gradients, under its colour if it has one. Each part comes at most once, so none none or
fixed scroll is invalid. A gradient the token holds is drawn as one more layer under the written
ones, at the initial position, size and repeat. A currentColor in the token's last layer,
center currentColor, is the text color where the token is used.
A bare background: var(--surface) takes away an image a weaker rule declared, whatever its token
holds, as in a browser.
flex: var(--grow) is flex: <number>: it grows by the token, shrinks by 1, and starts from a
basis of 0, once the token is set. With no token it is flex's initial value, 0 1 auto, whatever
a weaker rule set, as on the web, where a var() that cannot be substituted gives the property its
initial value; flex-grow, flex-shrink and flex-basis take theirs the same way. Yoga's own
shrink is 0, so this is the one place an unset token writes something rather than nothing.
A flex-basis that is a length or a percentage is committed as the size it is along its
container's main axis: a width in a row and a height in a column, over one written for that
axis. React Native reads a flexBasis once for a view and not again when it changes, where a size
is read on every layout, and the two come to the same box. A basis of 0, which flex: 1 sets,
is committed as a basis, and so is auto.
A length needs a unit, as in a browser: margin-top: 3 is dropped with a warning, and so is a
token holding a bare number where a length is read. 0 needs none, a bare number is a factor
inside calc(), and line-height takes a ratio, in the longhand, the font shorthand or a token,
which a descendant multiplies its own font size by, as on the web. An opacity outside 0 to 1 is
clamped into it, and a font-weight outside 1 to 1000 is dropped.
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, except a percentage added to a token for the properties below.
calc() may also add a length to a percentage, calc(100% - 2rem), for a box's width and
height, their minimums and maximums, its insets, its margins and its padding: Tailwind's
max-w-[calc(100%-2rem)]. And for translate, where the percentage is of the box's own size:
translate-x-[calc(100%-2px)], which is how a switch's thumb slides over. The length is in
points or rem, or a token, calc(100% - var(--handle)), which is settled where the box is and
follows the token changing. Yoga takes a number or a percentage
and not their sum, so the engine commits the box with the declaration set aside, reads the box
the percentage is of, and commits it at what the sum comes to, all in the one task. That is two
more commits when the box or anything in it changes, and one read on every commit after. A
percentage of a height is of a parent that was given a height; under an auto height the
declaration is set aside, as a browser sets it aside. In a grid item the percentage is of the
item's area. On a host with no layout to read the declaration is set aside too. Anywhere else,
font-size or gap say, the sum is dropped with a warning.
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, with two exceptions. color: inherit and color: unset are the color the
element inherits. And inherit is the parent's value for border-radius and its corners,
width, height and their minimums and maximums, font and the font properties,
letter-spacing, text-align, text-transform, background-color and opacity: the element
has none where its parent has none.
Layout is Yoga's, which follows CSS flexbox with a few differences 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.
Three more:
- A box whose margins are larger than its container is 0 high in Yoga, with its children overflowing it. A browser keeps the box as tall as its content.
- A percentage
min-heightis measured against the height of the box two levels up rather than the parent's:min-height: 50%in a 40-point box inside an 80-point one is 40 points, not 20. - A percentage
gapin a box with no fixed size along that axis grows the box by the gap. A browser resolves the percentage against the size the box ends up with.
A box with a z-index above 0 is drawn over what follows the boxes it is in, as in a browser: a
menu written in one card opens over the next card. A native view is ordered among the views
beside it only, so each view the box is in is given its z-index, up to a view with a z-index
of its own or one that is not a plain view, a scroll view say. Two differences follow. A view
raised this way is drawn whole over what follows it, its background too, where a browser raises
the box alone. And opacity and transform do not stop a box being raised past them.
Transitions, animate.enter/animate.leave, @keyframes and animation are their own page: see
Animation.
tint-color is native's own, with no counterpart on the web: the color an image is drawn in, and
the color of an SF Symbol. It takes the values color takes: a token, platform-color(), a
relative color or a color-mix(), light-dark(), and currentColor, which is the element's text
color. shadow-color takes the same.
A sheet read as a browser reads it
A sheet compiled for components written for a browser, as the Tailwind sheet is when its
browser option is set, reads a few things an app's own sheet is told to
write another way:
::beforeand::afterare boxes of their own, as in a browser: the engine keeps a view first in the element for::beforeand last for::after, styled by the rules written for it, wherever a rule gives it acontent. A stringcontentis the text the box holds, andcontent: ""an empty box;none, nocontent, or anattr()or counter draws no box. The box is no child to a selector that counts children,:first-childor:empty. Any other pseudo-element is dropped with a warning, as in an app's own sheet.visibility: hidden(andcollapse) draws the box with no opacity and takes no touch, and the box keeps its room.visiblefrom a later rule shows it again. Unlike in a browser, a hidden box hides all that is in it: a box inside it withvisibility: visibleis not seen either, as the opacity of a view is the opacity of everything in it.clip: rect()with nothing in it, andclip-path: inset(50%), draw nothing of the box over any rule that draws it - no background, shadow, border, outline or minimum size - and leave it there for a screen reader.clip-path: polygon(a 0, b 0, b 100%, a 100%), a band of the box from side to side withaandbpercentages, is kept for@ng-native/web-compat, which draws an<img>through the band: one picture over another, cut where a before-and-after slider is. No native view is sent it, and without that package nothing is drawn of it. A band with an edge that is a length or a token, and any other polygon, is dropped with a warning.Any other clip is dropped with a warning, as everywhere.
will-change,contain,perspectiveandbackdrop-filtermake aposition: staticbox the one an absolute box inside it is placed against, as in a browser: awill-changenamingtransform,translate,rotate,scale,perspective,filter,backdrop-filter,containorposition, acontainoflayout,paint,strictorcontent, and aperspectiveorbackdrop-filterother thannone. No native view is sent them. Native draws no perspective and no backdrop filter, and contains neither the size nor the paint of a box, which is said with a warning. Any of the four whose value reads a token is dropped with a warning.overflow-xoroverflow-yofhiddenorclipclips both axes, since a view clips on both or on neither.overflow-y: autoorscrollisoverflow: scroll, which also clips both axes: the box scrolls only where the element is a<scroll-view>, and a plain view does not. Any other value on one axis is no clip.:checkedand:indeterminatematch an element whose prop of that name is set, and:enabledone with nodisabledprop.
What has nothing to map onto
These are dropped with a warning that says so, not declarations that quietly do nothing:
floatsubgrid, and thegridandgrid-templateshorthandslist-style- Table layout:
table-layout,border-collapse,caption-side,empty-cells. A library's sheet read as a browser's keepstable-layout, for@ng-native/web-compat's tables - Multi-column layout
will-changeandcontain, which a sheet read as a browser's keeps for placing absolute boxes, above- CSS counters
clip-path, other than the shapes a sheet read as a browser reads it keeps, above
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.