Skip to content

Build log

Making a text field push content down, four times

The hardest behavior we have shipped, and why it had to be solved four separate times.

On the canvas, a multiline text field grows as you type and everything below it moves down. It takes about four seconds to draw and it is the single hardest thing we have had to generate.

Why it is hard

The easy version is a fixed box. Set a height, clip the overflow, ship it. Every design-to-code tool can do that, and it is wrong: the design said the field grows, and a field that clips is a different design.

The correct version needs the running app to measure text it has not rendered yet, in the right font at the right width, then move every element below by exactly that delta, then extend the scrollable area so the bottom of the form is still reachable. That is three separate problems, and none of the four targets solves them the same way.

There is no shared abstraction available here. The IR can describe that a field is multiline. It cannot describe how a platform measures text, because that is the platform's business.

Four solutions

React Native was the friendliest. onContentSizeChange hands you the content height as it changes. Take the delta, apply it to the offsets of everything below.

HTML was next. The browser already computed the answer: scrollHeight on the textarea. A small script listens for input and shifts the following elements. It is the only runtime code we emit, and it only appears when a design actually uses an auto-growing field.

Flutter required measuring rather than observing. The height has to be computed against the field's own font, size, and width before layout, so the widget below knows where to sit.

SwiftUI was the one that fought back. A plain fixed .frame clips, which is exactly the failure we were trying to avoid. The working answer was a TextEditor that measures its own wrapped text and grows to fit, with the scroll container sized to the full content height including that growth.

The part that surprised us

The scroll interaction was worse than the growth itself. A field that grows past the bottom of the device has to extend the scrollable content, or the user types into a box they cannot see the bottom of. Every target needed its scroll container sized against live content height rather than the static canvas height. Three of the four had a version that looked correct until you typed enough to need it.

How we know it works

Generated Flutter passes flutter analyze and flutter build web against the real SDK. Generated SwiftUI is type-checked with swiftc -typecheck against the iOS SDK. Both are run by a dedicated verification suite (npm run verify:native) rather than being a thing someone remembers to do. It sits outside the fast test suite because it needs the Flutter SDK and Xcode installed and takes up to a minute and a half.

That is not the same as proving the behavior is pixel-identical on device, and we are not going to claim it is. It proves the code we emit is real code that builds, which is a lower bar than the one we care about but a bar that a surprising amount of generated code fails.

Why bother

Because "looks like the design" and "behaves like the design" are different products. Static fidelity is a screenshot. If the field does not grow, the form is not the form you designed, however identical it looks before anyone types.