Lesson 1 of 5
Flexbox with Different Defaults
"Point of view is worth 80 IQ points." — Alan Kay, Creative Think (1982)
You already know flexbox. That is the problem.
React Native's layout engine is Yoga, a C++ implementation of the flexbox algorithm, and it is a good one — the same rules about main and cross axes, the same justifyContent and alignItems, the same growing and shrinking of items along a line. Nothing you learned about flexbox on the web is wrong here.
What is different is where it starts. A handful of properties have different initial values, and because defaults are the values you never write down, the differences are invisible in the code and obvious on the screen. You write the same JSX you would have written in HTML, and the result is rotated ninety degrees, or overflowing its container, or refusing to fill the space you meant it to fill.
The good news is that the list is short and Yoga will tell you what is on it, because Yoga has a switch for being a browser.
Yoga ships a configuration flag called UseWebDefaults that makes it behave like a browser, and React Native does not turn it on. Every difference in this chapter is therefore a deliberate choice rather than an incompatibility. What would a layout engine designed for phones rather than documents want to do differently, and is a vertical default actually the right call?
Yoga will tell you what it changed
Start with the primary source, because it is unusually direct. Yoga's Node class has a method that exists solely to undo the differences:
void useWebDefaults() {
style_.setFlexDirection(FlexDirection::Row);
style_.setAlignContent(Align::Stretch);
}
Two lines. Plus one constant elsewhere in Style.h:
static constexpr float DefaultFlexShrink = 0.0f;
static constexpr float WebDefaultFlexShrink = 1.0f;
So by Yoga's own accounting, becoming a browser takes exactly three changes: flexDirection, alignContent, and flexShrink. Everything else in the algorithm is already the same, and the flag is not even switchable after the fact — setConfig asserts that UseWebDefaults may not be changed after constructing a Node.
React Native never calls it. I checked the renderer for setUseWebDefaults and there is no call site, which is why the differences below are permanent rather than configurable.
It is the difference between a page and a receipt. A page of a book is a landscape of lines running left to right, and everything about typesetting assumes that horizontal flow — which is why a browser's default is a row and why HTML has a whole vocabulary of inline and block. A till receipt is a column. Things arrive one under another, the width is fixed by the paper, and the only interesting question is how far down the roll you have got. A phone screen is far closer to a receipt than to a page: it is narrow, it is tall, it scrolls in one direction, and almost every screen you will ever build is a vertical stack of horizontal strips. Yoga's default is the receipt. The engine is the same, the paper is a different shape, and the starting assumption follows the paper.
Here is the whole difference, as data.
The last two deserve a sentence each before we move on. boxSizing defaults to border-box, so a View with width: 100, padding: 10 and borderWidth: 5 is a hundred points wide and has seventy points of content — I measured that against Yoga directly and got 70. Web developers have been writing box-sizing: border-box as the first rule of every stylesheet for fifteen years; React Native simply starts there. And there is no position: static: the values are relative and absolute, relative is the default, and every view is therefore already a positioning context for its absolute children.
Column, not row
The most visible difference is the first one, and it has a good reason behind it.
The first frame is what a browser would have given you for display: flex with flex-direction: column written explicitly. The second is what a browser gives you for free.
Two consequences follow from the flip, and they are the ones that trip people up in the first week. justifyContent controls the vertical distribution by default, and alignItems controls the horizontal one, which is the reverse of the web reflex. And alignItems: "stretch" — a default in both systems — now stretches children across the width rather than the height, which is why a View with no width fills its parent horizontally without you asking. That is usually what you wanted, and it is worth knowing you did not ask for it.
The reason is the shape of the device. A phone screen is roughly twice as tall as it is wide, and almost every screen in every app is a vertical stack: a header, some content, a footer. Choosing column means the common case needs no property at all, and the horizontal case — a row of chips, an icon next to a label — is where you write something down.
Nothing shrinks unless you ask
This is the difference that produces the bug reports, because it does not look like a layout choice. It looks like the layout is broken.
In CSS, flex-shrink defaults to 1. Items in a flex line that do not fit are shrunk proportionally until they do, which is why a row of fixed-width boxes in a narrow container squeezes rather than spills. In Yoga, flex-shrink defaults to 0. Nothing shrinks. Items keep their size and overflow the container.
I measured it: two children with width: 250 in a row 300 wide, laid out by Yoga 3.2.1, computed to [250, 250]. A browser would give [150, 150].
The second frame is not a fix so much as an opt-in. And the opt-in is worth understanding rather than pasting everywhere, because there is a reason for the default: on a phone, silently shrinking a control below its designed size is often worse than overflowing, since a squeezed button becomes untappable and squeezed text becomes unreadable. Overflow is at least visible. Shrinkage is a quiet degradation you may not notice until a user with a long name or a large text setting sends a screenshot.
The practical rule is that flexShrink: 1 belongs on the element that should absorb the pressure — usually the flexible middle of a row — and not on the fixed things around it. A row with an avatar, a name and a timestamp wants the name to shrink and the other two to keep their size, which is one flexShrink: 1 rather than three.
Yoga started life inside React Native as css-layout, a from-scratch implementation of the flexbox specification written because there was no other option: iOS and Android had nothing resembling flexbox, and shipping a browser engine to lay out a native view tree was not on the table. It was originally written in JavaScript and transpiled to C, which is an unusual origin for a layout engine and explains some of its shape. Being an independent implementation rather than a port is also why the defaults could diverge at all — nobody was porting a browser's initial values, so the initial values were chosen fresh for the platform. It was extracted, renamed to Yoga and released as a standalone library in 2016, and has since been used well outside React Native, including by Litho, ComponentKit and a number of engines with no relationship to React. The UseWebDefaults flag exists precisely because of those other users: if you are embedding Yoga in something document-shaped, you can have the browser's starting values, and React Native is the caller that declines them.
What flex: 1 actually means
Now the finding that sent me to a second source, because it contradicts React Native's own documentation.
flex is a shorthand, and Yoga expands it in three separate methods rather than one. Reading them in order:
float Node::resolveFlexGrow() const {
if (owner_ == nullptr) return 0.0; // root nodes never grow
if (style_.flexGrow().isDefined()) return style_.flexGrow().unwrap();
if (style_.flex().isDefined() && style_.flex().unwrap() > 0.0f) return style_.flex().unwrap();
return Style::DefaultFlexGrow; // 0
}
Style::SizeLength Node::processFlexBasis() const {
Style::SizeLength flexBasis = style_.flexBasis();
if (!flexBasis.isAuto() && !flexBasis.isUndefined()) return flexBasis;
if (style_.flex().isDefined() && style_.flex().unwrap() > 0.0f) {
return config_->useWebDefaults() ? StyleSizeLength::ofAuto() : StyleSizeLength::points(0);
}
return StyleSizeLength::ofAuto();
}
float Node::resolveFlexShrink() const {
if (owner_ == nullptr) return 0.0;
if (style_.flexShrink().isDefined()) return style_.flexShrink().unwrap();
if (!config_->useWebDefaults() && style_.flex().isDefined() && style_.flex().unwrap() < 0.0f) {
return -style_.flex().unwrap();
}
return config_->useWebDefaults() ? Style::WebDefaultFlexShrink : Style::DefaultFlexShrink;
}
Read the third one carefully. With web defaults off, a positive flex never reaches the shrink branch — that branch only fires for a negative flex — so the method falls through to DefaultFlexShrink, which is 0.
Which means flex: 1 in React Native is flexGrow: 1, flexShrink: 0, flexBasis: 0. The documentation says flexShrink: 1.
I nearly published the documentation's version. React Native's guide states that flex: <positive number> equates to flexGrow: <number>, flexShrink: 1, flexBasis: 0, I have believed that for years, and it appears in a great deal of teaching material. Reading resolveFlexShrink made me doubt it, but a source read is not proof — I could have been looking at a code path React Native overrides, or a version that had moved on. So I installed yoga-layout and built the smallest experiment that could distinguish the two claims: two children with an explicit flexBasis of 250 in a container 300 wide, once with flex: 1 and once with flex: 1 plus an explicit flexShrink: 1. If the shorthand implied shrinking, the results would match. They do not: [250, 250] against [150, 150]. The engine and the documentation disagree, and the engine is what runs. The lesson I keep relearning in this volume is that measuring is cheap and being confidently wrong in print is not.
Practically this matters less often than it sounds, because flex: 1 also sets flexBasis: 0, and an item whose basis is zero has nothing to shrink from — it is sized entirely by growing. You notice the difference only when a basis or a size is also in play. But when you do notice it, nothing else explains it, and the documentation will actively mislead you.
alignContent, and what wrapping does
The third switchable difference is the one people meet least often, because it only applies when a container wraps.
alignContent distributes the lines of a wrapped flex container along the cross axis, and CSS starts it at stretch while Yoga starts it at flex-start. So in a browser, wrapped rows spread out to fill the container's height; in React Native they pack against the top and leave the remainder empty.
Measured on the same tree — four boxes of 120 by 40 wrapping in a 200 by 200 container — the tops come out as [0, 40, 80, 120] with Yoga's default and [0, 50, 100, 150] with alignContent: "stretch".
The left frame packs its four rows against the top with a gap at the bottom. The right frame spreads them. Neither is wrong; the left is simply the one you get without asking, and if you have ever wondered why a wrapped grid in React Native does not fill its container the way the same code did on the web, this is the entire reason.
The bug that reaches production from this chapter is almost always a long string in a row. A message row with an avatar, a display name and a timestamp works perfectly for months, and then a user with a genuinely long name pushes the timestamp off the right edge of the screen — where it is not clipped, because overflow defaults to visible, it is simply drawn outside the parent and invisible against the screen edge. The fix is one property in one place: flexShrink: 1 on the name, usually with numberOfLines={1} so it truncates rather than wraps. What makes this worth naming is that it is invisible in every test you are likely to write. Your fixtures have short names, your simulator is wide, and the failure needs a real user with a real name at a real text size. The second version of the same bug is a row of buttons whose labels are longer in German or Portuguese than in English, which is the localisation team's first bug report on every project I have seen.
What is simply not there
Finally, the absences, because knowing them saves you searching.
There is no display: block or display: inline. display takes flex and none, and every View is a flex container — there is no non-flex layout mode to fall back to. There is no float, no CSS Grid, no multi-column layout. There are no media queries, because there is no stylesheet to attach them to; the equivalent is reading useWindowDimensions and branching in JavaScript, which is the next chapter but one. There is no calc(), and percentages are supported for sizes, margins, padding and flexBasis but not everywhere.
What is there and is easy to miss: gap, rowGap and columnGap work, and are the right way to space children rather than margins on every child. aspectRatio works as a first-class style property — Yoga had it years before CSS did — and a width of 200 with aspectRatio: 16 / 9 computes to a height of 113. And margin: "auto" works for centring and pushing along the main axis: an item 50 wide with marginLeft: "auto" in a 300-wide row lands at 250, which is the idiom for "push this to the end" without a spacer view.
Common Mistakes
- Assuming a row.
flexDirectiondefaults tocolumn, so children stack vertically until you say otherwise. - Reaching for
justifyContentto centre horizontally. In a column it centres vertically;alignItemsis the horizontal one. - Expecting items to shrink.
flexShrinkdefaults to0, so a row that does not fit overflows rather than squeezing. - Believing
flex: 1setsflexShrink: 1. It does not — the engine resolves it to0, whatever the documentation says. - Putting
flexShrink: 1on everything in a row. Put it on the element that should absorb the pressure, not the fixed ones around it. - Expecting overflow to be clipped.
overflowdefaults tovisible, so children are drawn outside their parent unless you sethidden. - Adding padding to a fixed width and expecting the box to grow. Sizing is
border-box, so the padding comes out of the width. - Looking for
position: static. There is none; every view isrelativeand therefore already a containing block. - Wondering why a wrapped grid does not fill its height.
alignContentstarts atflex-start, notstretch. - Spacing children with a margin on each.
gapexists and does not produce a stray margin on the last child.
Interview Corner
flexDirection, which is column rather than row; flexShrink, which is 0 rather than 1, so nothing shrinks unless you ask; and alignContent, which is flex-start rather than stretch, so wrapped lines pack rather than spread. Beyond those, sizing is border-box by default and there is no position: static — every view is relative. The practical effect of the first is that justifyContent and alignItems swap axes relative to the web reflex, and the practical effect of the second is that a row of fixed-width items overflows instead of squeezing.flexShrink: 1 to the name, and usually numberOfLines={1} so it truncates rather than wrapping. The cause is that flexShrink defaults to zero in Yoga, so when the three children do not fit the row they keep their sizes and overflow — and because overflow also defaults to visible, nothing clips them, so the timestamp is drawn past the screen edge rather than being cut off visibly. I would put the shrink only on the name rather than on all three, because the avatar and the timestamp have designed sizes that should not degrade, and the name is the element with slack in it. It is worth adding that this is invisible in most test data: it needs a genuinely long name, or a longer translation, or a user at a large accessibility text size.UseWebDefaults configuration flag that flips exactly three of them — flexDirection, alignContent and flexShrink — and React Native never calls it.UseWebDefaults exists for.Hands-on
Rebuild the resolution rules this chapter measured. Implement initialStyle so a fresh node carries Yoga's declared defaults, and useWebDefaults so it carries the browser's — remembering that the flag changes exactly three of them; expandFlex as the three Yoga methods do it, including the negative-flex branch that only exists when web defaults are off and the root-node rule that nothing grows or shrinks without an owner; contentWidth for border-box sizing; and distribute, which lays a row of items into an available width, growing by flexGrow when there is slack and shrinking by flexShrink when there is not, and overflowing when nothing may shrink. The checks below run either way — a stub scores zero, so a passing run means the behaviour is real.
Summary
- Yoga is a real flexbox implementation with different initial values, so the algorithm you know applies and the starting point does not.
Node::useWebDefaults()is two lines plus one constant, so by Yoga's own accounting exactly three defaults separate it from a browser.- React Native never calls it, which makes every difference in this chapter deliberate and permanent rather than configurable.
flexDirectiondefaults tocolumn, which swaps which axisjustifyContentandalignItemscontrol.flexShrinkdefaults to0, so a row that does not fit overflows instead of squeezing — the source of most first-week layout bugs.flex: 1resolves toflexGrow: 1, flexShrink: 0, flexBasis: 0, contradicting React Native's documentation, verified againstyoga-layout3.2.1.alignContentdefaults toflex-start, so wrapped lines pack at the start rather than stretching to fill.- Sizing is
border-boxand there is noposition: static, neither of which has a browser-compatibility mode. overflowdefaults tovisible, so an overflowing row is drawn outside its parent rather than clipped.gap,aspectRatioandmargin: "auto"all work, and are usually better than the workarounds people reach for first.
In the next chapter, StyleSheet Is Not CSS, we look at the other half of the resemblance — an API that borrows CSS's property names and almost none of its semantics, with no cascade, no selectors, no inheritance beyond the one case from Part II, and a create function whose actual job is not what its name suggests.
The thing I want to hold on to from this chapter is not the list of defaults, it is what happened when I checked one. I read resolveFlexShrink, saw that a positive flex could not reach the shrink branch, and immediately doubted myself — because the claim I was contradicting is in React Native's own documentation and in a great deal of material written by people who know this framework far better than I do. The honest position at that moment was not "the docs are wrong", it was "I do not know, and this is cheap to find out". Twenty minutes and one npm install later I had two numbers that could only be explained one way. What I find worth saying out loud is that the doubt was correct and so was the check. A source read tells you what one file says; it does not tell you whether some other layer overrides it, or whether you are reading a version that shipped. Running the thing tells you what happens. I have written four chapters in this Part now where the interesting content came from a measurement rather than a memory, and the pattern is consistent enough that I would put it as advice: when a source read contradicts something you believe, that is not a conclusion, it is a hypothesis with a very short path to being tested.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.