Knit UI
GitHub

Cross-platform authoring

The kit hides most platform difference. This page is about the rest, because the parts that leak are worth knowing before they bite.

You should not need Platform.OS

If you find yourself branching on platform in app code to use a kit component, that is a gap — open an issue. Inside the kit, divergence is handled by splitting files, not by runtime branches:

PatternWhen
Thing.tsxidentical everywhere
Thing.tsx with an isWeb branchone property has no native equivalent (position: "fixed")
Thing.tsx + Thing.native.tsxgenuinely different implementations
+ Thing.shared.tsboth platforms delegate to one pure module

.shared is the pattern to copy in your own code: keep the logic platform-free and pure, and let two thin files wire it to Reanimated worklets or to requestAnimationFrame.

Layout: React Native's defaults win

flexDirection defaults to column, alignItems to stretch, flexShrink to 0 — on all platforms, including the web. That is the single biggest adjustment for a web developer. See Layout.

There is no position: fixed on native; Affix renders absolute there and pins to the nearest filling ancestor instead of the viewport.

Text is not a div

On native, text must be inside a text node — you cannot put a View inside a Text. The kit's Text handles nesting for you (string children are wrapped, element children are placed), but if you build your own text-ish component, mixing element and string children is where it breaks.

Gestures and animation run somewhere else

Animation on native runs as a worklet on the UI thread. Two consequences:

  • values you animate must live in shared values, not React state;
  • you cannot close over arbitrary JS in an animation callback.

For Skia, keep data in shared values and build the SkPath inside useDerivedValue. A path object in a shared value works on native and breaks on the web.

The keyboard

Handled with the platform Keyboard API plus KeyboardAvoidingView / KeyboardAwareScrollView — no third-party keyboard controller. Two Android specifics the kit already absorbs, and that you will hit if you hand-roll:

  • a tap in a scrollable list while the keyboard is dismissing is swallowed unless keyboardShouldPersistTaps="handled";
  • a readOnly (editable={false}) TextInput receives no press events, so use focus rather than press to drive an attached overlay.

Measurement

Measured layout arrives after first paint on native, and a clipped element measures as zero. Any height animation therefore needs the content pinned absolute while it measures — which is what Collapse does.

On the web, use a callback ref for size observation, not a one-shot effect, or an element that mounts later never gets measured.

Drag maths

Use pageX/pageY minus a measured origin. locationX/locationY are relative to whichever view received the touch, so as soon as the finger crosses a child boundary the numbers jump — visible as a thumb that flickers.

Resource limits are real

  • Browsers cap live WebGL contexts (about 16). Skia canvases each hold one.
  • One shared <audio>/<video> element per medium in @knitui/media, teleported into the active player.
  • Long lists need windowing on device far earlier than on desktop web.

The docs site itself respects all three: canvases and players mount one at a time, and examples mount as they scroll into view.

Test on the thing

jsdom tests catch API mistakes, not platform ones. This repo runs its stories in three surfaces — Storybook, an Expo app and a Next app — precisely because a component can be correct in one and wrong in another. Do the same before shipping: if you have not seen it on Android, you do not know that it works.