Styles are JavaScript objects. There is no CSS file, no cascade, no var(), no media
queries and no pseudo-classes. That sounds like a loss and mostly is not — but it changes what you
have to build before you can build a screen.
StyleSheet.create
You can pass a plain object to style and it works. StyleSheet.create is
still the convention:
const styles = StyleSheet.create({
pressable: { flex: 1 },
pressed: { opacity: 0.8 },
card: { padding: 0, overflow: 'hidden', flex: 1 },
thumb: {
height: 96,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.colors.primarySoft,
},
emoji: { fontSize: 40 },
body: { padding: theme.spacing.md, flex: 1 },
description: { marginTop: 2, minHeight: 34 },
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: theme.spacing.sm,
},
cta: { fontWeight: theme.fontWeight.semibold },
});Two reasons. It validates the keys, so a typo is caught rather than silently ignored. And it
creates the object once, at module load, instead of on every render — an inline object is a
new object each time, which defeats the prop comparison that memo relies on. Lesson 21
returns to that.
Note the units: there are none. height: 96 is 96 density-independent pixels. No
px, no rem, no percentages except as strings like '90%'.
Names that differ from CSS
Properties are camelCase, and a few are simply different. backgroundColor, not
background-color. There is no shorthand — border: 1px solid red is three
properties. marginHorizontal and paddingVertical exist and are used
constantly. Text styles like fontWeight must be strings:
fontWeight: 700 is a type error, '700' is not.
Why you need a design system on day one
Because nothing inherits. Every screen names its own colours and sizes, so without a shared source of truth an app accumulates eleven greys and four different card radii within a fortnight. On the web, a stylesheet and a cascade absorb that carelessness; here nothing does.
The demo app defines its palette as plain constants:
export const palette = {
red: '#d8102a',
redDark: '#ab0d21',
redSoft: '#fdeaec',
black: '#231f20',
cream: '#fff8f0',
white: '#ffffff',as const at the end of that object matters more than it looks. Without it
TypeScript widens '#d8102a' to string, and the palette stops being a
closed set that autocomplete and exhaustiveness checks can work with.
⚠️ Note redDark is written out rather than computed. Sass can do arithmetic on a
colour with color.adjust; JavaScript cannot, so the shade is precomputed. This is one
of the few places where losing CSS actually costs something.
Two layers, not one
Raw tokens are not what components should import. A semantic layer sits over them:
export const theme = {
colors: {
primary: palette.red,
primaryDark: palette.redDark,
primarySoft: palette.redSoft,
onPrimary: palette.white,A component asks for theme.colors.textMuted, never palette.grey600.
The indirection is what makes a retune possible — change what "muted text" means in one place and
every screen follows, whereas a find-and-replace on grey600 would also hit the borders
that merely happen to share the value.
A spacing scale
export const spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 24,
xxl: 32,
xxxl: 48,
} as const;Naming the steps is what stops padding: 13 appearing next to
padding: 12 and quietly breaking the rhythm.
Shadows are genuinely platform-split
The one place where "write once" fully breaks down in styling:
export const shadow = {
card: {
shadowColor: palette.black,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 2,
},iOS draws shadows from the four shadow* properties and ignores
elevation. Android does exactly the reverse. Setting only one is why so many apps have
beautiful cards on iOS and flat ones on Android — and you will not notice unless you look at both.
Defining it once in the theme is how you stop having to remember.
Conditional styles
Style arrays merge left to right, and a false entry is skipped. That gives you the
whole conditional vocabulary:
export function Card({ flush = false, style, ...rest }: CardProps) {
return <View style={[styles.card, !flush && styles.padded, style]} {...rest} />;
}Base style, a conditional one, then the caller's override last. That last position is a convention worth keeping everywhere: it is what lets a component be reused without being forked.
What about the alternatives?
NativeWind brings Tailwind's class names to React Native, and styled-components works too. Both are reasonable. Neither removes the underlying facts — no cascade, no inheritance, platform-split shadows — they just change the syntax you express them in. Learning the plain version first means the abstractions make sense rather than hiding something you never saw.
What is next
Flexbox and Layout — the only layout system there is, and the three defaults that differ from the web.