Screen
The unit of routing — and the place where mobile-app primitives live
<Screen> is what each route renders. It's a fixed-position container with slots for app bars,
navigation bars, and safe-area aware status bars — the same vocabulary you'd use building a
native app.
Anatomy
Status Bar
App Bar
Navigation Bar
System Navigation Bar
- Status Bar
Top safe area. Reserves space for the notch and dynamic island.
statusBarHeightstatusBarColorhideStatusBar- App Bar
Top bar — set per-screen, or as a shared bar that stays mounted across transitions.
appBarsharedAppBar- Children
The screen body. Scrolls by default — disable it only when you bring your own scroll container.
contentScrollable- Navigation Bar
Bottom bar. Same per-screen and shared options as App Bar.
navigationBarsharedNavigationBar- System Navigation Bar
Bottom safe area. Reserves space for Android's system navigation bar.
systemNavigationBarHeightsystemNavigationBarColorhideSystemNavigationBar
Per-screen vs shared
App bar and navigation bar can be set in two ways. They mount at different times and behave differently across transitions.
appBar/navigationBar— the bar belongs to that one screen. It mounts and unmounts with the screen. Use this for headers and actions that change page-by-page.sharedAppBar/sharedNavigationBar— the bar persists across screens. It stays put during push and pop transitions, so it never re-animates. Use this for global UI like a bottom tab bar or a fixed header. Pass the same node from each screen and flemo bridges it through the transition.
Minimum
<Screen>
<h1>Hello</h1>
</Screen>The content area scrolls by default. Background is white unless you change it.
App bar and navigation bar
Two slots, two flavors each: per-screen (appBar, navigationBar) and shared
(sharedAppBar, sharedNavigationBar).
The shared variants stay mounted across screen transitions, so the bar doesn't re-animate every push. Use them for global UI like a bottom tab bar.
<Screen
appBar={<TopBar title="Inbox" />}
navigationBar={<BottomActions />}
sharedNavigationBar={<TabBar />}
>
<MailList />
</Screen>Safe areas
<Screen
statusBarHeight="env(safe-area-inset-top)"
statusBarColor="#000"
systemNavigationBarHeight="env(safe-area-inset-bottom)"
systemNavigationBarColor="#000"
>
…
</Screen>Pass hideStatusBar or hideSystemNavigationBar to collapse the area when you don't want it
reserved (e.g., a full-bleed media screen).
Background
<Screen backgroundColor="#0b0b0c">…</Screen>Defaults to "white". Any CSS color value works.
Disabling content scroll
If you're rendering your own scroll container, opt out of the built-in one:
<Screen contentScrollable={false}>
<CustomScrollArea />
</Screen>All Screen props
| Prop | Type | Default | Notes |
|---|---|---|---|
appBar | ReactNode | — | Per-screen top bar |
navigationBar | ReactNode | — | Per-screen bottom bar |
sharedAppBar | ReactNode | — | Top bar that survives transitions |
sharedNavigationBar | ReactNode | — | Bottom bar that survives transitions |
backgroundColor | string | "white" | CSS color |
statusBarHeight | string | — | CSS length, usually env(safe-area-inset-top) |
statusBarColor | string | — | CSS color |
hideStatusBar | boolean | false | Don't reserve the top safe area |
systemNavigationBarHeight | string | — | CSS length, usually env(safe-area-inset-bottom) |
systemNavigationBarColor | string | — | CSS color |
hideSystemNavigationBar | boolean | false | Don't reserve the bottom safe area |
contentScrollable | boolean | true | Whether the children area scrolls |
LayoutScreen — morph between screens
A list thumbnail that unfolds into the full image on the next screen. In flemo, four pieces work as one:
| Piece | Goes where |
|---|---|
<LayoutScreen> | The destination screen |
<LayoutConfig> | The outermost wrapper of the morphing container |
transitionName: "layout" | The navigate.push option |
layoutId | The push option and the motion element prop on both screens |
What each piece does
-
<LayoutScreen>— drop-in replacement for<Screen>on the destination. Keeps layoutId pairing alive across the unmount when you navigate back. The source (list) screen can stay a plain<Screen>. -
<LayoutConfig>— keeps the morph's timing (duration, ease) in sync with the current transition preset. Without it, the morph runs on a default spring that falls out of step with the screen transition. Put it as the outermost wrapper of whatever should morph. -
transitionName: "layout"— picks a screen transition that fades just enough to keep the morph readable. Cupertino's slide would cover it up. -
layoutId— the key that pairs the same element across two screens. ThelayoutIdyou pass tonavigate.pusharrives on the destination asuseScreen().layoutId. Tag the container, image, title, and price with a shared prefix and they morph as one unit.
Example — gallery → detail
import { Screen, useNavigate } from "flemo";
import { motion } from "motion/react";
function Gallery() {
const navigate = useNavigate();
return (
<Screen>
{photos.map((p) => (
<motion.div
key={p.id}
layoutId={`photo-card-${p.id}`}
onClick={() =>
navigate.push("/photos/:id", { id: p.id }, { transitionName: "layout", layoutId: p.id })
}
>
<motion.img layoutId={`photo-image-${p.id}`} src={p.thumb} />
<motion.span layoutId={`photo-title-${p.id}`}>{p.title}</motion.span>
</motion.div>
))}
</Screen>
);
}import { LayoutConfig, LayoutScreen, useScreen } from "flemo";
import { motion } from "motion/react";
function Photo() {
const { layoutId } = useScreen();
return (
<LayoutScreen>
<LayoutConfig>
<motion.div layoutId={`photo-card-${layoutId}`} className="fixed inset-0">
<motion.img layoutId={`photo-image-${layoutId}`} src={photo.full} />
<motion.h1 layoutId={`photo-title-${layoutId}`}>{photo.title}</motion.h1>
</motion.div>
</LayoutConfig>
</LayoutScreen>
);
}Common pitfalls
- Forgot
<LayoutConfig>— the morph runs on Motion's default spring, out of sync with the screen transition. - Missed
transitionName: "layout"— the default cupertino slide covers the morph; it just looks like a normal push. - Mismatched
layoutIds — pairing fails and nothing morphs. The string the source builds must be byte-for-byte identical to the one the destination builds fromuseScreen().layoutId. - Only the parent has a
layoutId— the inner image and text fade in separately and look off. Tag every element that should travel together with the same prefix.