How does React Native Web turn StyleSheet.create styles into CSS, and what does the browser receive for a plain inline style object?
answer
- one rule per property and value
- r- prefix plus a hash
- rightmost style wins per property
- plain object: inline style attribute
- shadow* rewritten into boxShadow
basics
~20 sReact Native Web compiles each StyleSheet.create declaration into atomic CSS: one deduplicated class per property-value pair, inserted once into a shared style sheet. A plain object that never went through create becomes an inline style attribute.
solid answer
~40 s`StyleSheet.create` in `react-native-web` compiles every property-value pair into its own **atomic class** (`r-<prop>-<hash>` in development, `r-<hash>` in production), inserts the rule once into a `<style id="react-native-stylesheet">` element and caches it, so every component using `opacity: 0.5` shares one rule. At render, a style array is resolved per property with the rightmost value winning, and classes for overridden properties are dropped, which keeps React Native's merge semantics. A plain object is preprocessed and emitted as an inline `style` attribute. Shorthands sit in an earlier insertion group than longhands, so `marginTop` beats `margin` regardless of array order. There are no selectors, media queries or pseudo-classes, and legacy `shadow*` props are rewritten to `boxShadow` with a development warning.
code
tsx · 15 linesimport { StyleSheet, View } from 'react-native';
const styles = StyleSheet.create({
card: {
padding: 12,
borderRadius: 8,
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.2)',
},
dimmed: { opacity: 0.5 },
});
export function Card({ dimmed, width }: { dimmed: boolean; width: number }) {
// styles.card and styles.dimmed become atomic classes; { width } becomes an inline style
return <View style={[styles.card, dimmed && styles.dimmed, { width }]} />;
}go deeper
Recall that styles are still written as objects in the style prop and that React Native Web turns them into real CSS for the browser.
Explain atomic classes, the cache that deduplicates rules, the rightmost-wins resolution of style arrays, and the difference between created and inline styles.
Discuss the consequences: no selectors or media queries, shorthand validation, shadow* rewritten to boxShadow, and why static styles belong in create for render cost and stylesheet size.
Weigh atomic CSS as the price of one styling model across platforms: small, predictable CSS and no cascade bugs, against losing class-based theming and existing CSS tooling.
## Two paths: compiled and inline In **React Native Web** a `style` prop can hold three kinds of values, and they take different paths to the browser: - An entry created by **`StyleSheet.create`** is compiled once, at module load, into CSS classes. - A **plain object** (`style={{ width: w }}`) is preprocessed at render and written as an **inline `style` attribute**. - An **array** can mix both; it is resolved into one `class` string plus at most one inline style object. The library's own docs recommend `StyleSheet.create` for anything static: it produces opaque references that are cheap to pass around, and it avoids re-serialising the same object on every render. ## What an atomic class is `create` splits each style object into **one CSS rule per property-value pair** (direction-sensitive properties such as `marginStart` get a left-to-right and a right-to-left variant) and names each rule with a hash: ```typescript const styles = StyleSheet.create({ card: { opacity: 0.5, padding: 12 }, badge: { opacity: 0.5 } }); // development: class="r-opacity-… r-padding-…" on card, class="r-opacity-…" on badge // production: class="r-…" names without the property part ``` - The class name is `r-<property>-<hash>` in development and `r-<hash>` in production. - Rules are cached by property and value, so `opacity: 0.5` produces **one** rule however many components use it. The style sheet grows with the number of distinct declarations, not the number of components. - The rules live in a single `<style id="react-native-stylesheet">` element that the library manages. - In development, `create` also validates and freezes each object. It reports, and removes, declarations the web port cannot honour: multi-value shorthands such as `margin: '8px 16px'`, values containing `!important`, and CSS shorthands such as `font` or `background` that React Native never had. Internal reset styles (the `View` and `Text` defaults) are the exception: they compile to one classic class each, like `css-view-…`. ## How a style array is resolved React Native merges a style array left to right, last value wins. React Native Web keeps that contract with class names: 1. It walks the array from the **rightmost** entry to the left. 2. For each property it keeps only the first value it meets, so the **rightmost value wins**. 3. Classes for properties that were overridden are dropped, so the element never carries two competing `opacity` classes. 4. If an inline object sets a property later in the array, the class for that property is dropped and the inline value is used; if the inline object comes earlier, the later class wins and the inline value is discarded. Because every compiled class sets exactly one property, specificity fights and source-order surprises of hand-written CSS do not arise. ## Shorthand versus longhand ordering Atomic classes still need a rule for shorthands. React Native Web inserts rules into ordered groups: shorthands such as `margin`, `padding`, `borderWidth` and `flex` go into an earlier group than longhands such as `marginTop`. So `[{ marginTop: 20 }, { margin: 0 }]` still ends with a 20-pixel top margin. That matches React Native, where the more specific edge also wins over the all-edges value. ## Style props that are rewritten or ignored | React Native style | On the web | |---|---| | `shadowColor`, `shadowOffset`, `shadowOpacity`, `shadowRadius` | combined into one `boxShadow` value; development warns `"shadow*" style props are deprecated. Use "boxShadow".` | | `textShadowColor`, `textShadowOffset`, `textShadowRadius` | combined into `textShadow`, with a similar warning | | `elevation` (Android) | ignored | | `marginHorizontal`, `paddingVertical`, `start`, `end` | mapped to logical CSS such as `marginInline`, `paddingBlock` and inline insets | | `transform: [{ scale: 2 }]` | serialised to `transform: scale(2)` | `boxShadow` is also a React Native style prop on the New Architecture (iOS, and Android 9 and later for outset shadows), so a card written with `boxShadow` renders one shadow definition on all three platforms. The `shadow*` warning is React Native Web nudging you there; it is printed once per session and only in development. ## Why atomic CSS The design goal is predictable styling at scale. With one rule per declaration, the style sheet stops growing once most property-value pairs have been seen, every element's final style is decided by the resolved array rather than by selector specificity, and there is no cascade between components. The cost is that the HTML carries long lists of short class names and that familiar CSS tooling (selectors, class-based themes) has nothing to hook into. ## What you cannot express - No selectors, `:hover` or other pseudo-classes, and no `@media` queries. Hover comes from `Pressable`'s `onHoverIn` and `onHoverOut`; breakpoints come from `useWindowDimensions`. - No `className` prop on `View`, `Text` or the components built on them. - `StyleSheet.hairlineWidth` is `1` on the web, because browsers may round a sub-pixel border to zero.
- In React Native Web, why does a style with shadowOffset and shadowRadius print a warning, and what should a cross-platform card use instead?React Native Web converts the four `shadow*` props into one `boxShadow` value and warns in development that they are deprecated on the web. React Native itself supports `boxShadow` on the New Architecture, so writing `boxShadow` once gives the same shadow on iOS, Android 9 and later, and the browser. `elevation` is ignored on the web.
- In React Native Web, what does the development build do with margin: '8px 16px' inside StyleSheet.create?It logs `Invalid style property of "margin"` with a note that only single values are supported, and removes the declaration. React Native Web accepts shorthands such as `margin` only with one value; per-edge values must use longhands like `marginVertical` or `marginTop`, as on native.
- How do you express a hover style or a breakpoint with React Native Web, given there are no selectors or media queries?Hover comes from interaction state: `Pressable`'s `onHoverIn` and `onHoverOut` callbacks (and, on the web, a `hovered` flag in its style function) drive a style choice in JavaScript. Breakpoints come from `useWindowDimensions`, picking a style entry by width. Both stay inside React Native's model, so the same code runs on phones where hover simply never happens.
Atomic CSS works like a printer's type case: each letter is cast once and every page is composed by picking the pieces it needs, instead of casting a new plate for every page. A new component reuses existing classes and only adds the few declarations nobody has used yet.
saying these in an interview costs you the question
- StyleSheet.create generates one class per component, like CSS modules.
- Inline style objects are compiled to classes at render time too.
- Array order does not matter because CSS specificity decides.
- A media query string inside StyleSheet.create works on the web build.
- shadow* props are silently ignored on the web.