flemov1.5.7

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

Childrenscrollable

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

PropTypeDefaultNotes
appBarReactNodePer-screen top bar
navigationBarReactNodePer-screen bottom bar
sharedAppBarReactNodeTop bar that survives transitions
sharedNavigationBarReactNodeBottom bar that survives transitions
backgroundColorstring"white"CSS color
statusBarHeightstringCSS length, usually env(safe-area-inset-top)
statusBarColorstringCSS color
hideStatusBarbooleanfalseDon't reserve the top safe area
systemNavigationBarHeightstringCSS length, usually env(safe-area-inset-bottom)
systemNavigationBarColorstringCSS color
hideSystemNavigationBarbooleanfalseDon't reserve the bottom safe area
contentScrollablebooleantrueWhether 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:

PieceGoes where
<LayoutScreen>The destination screen
<LayoutConfig>The outermost wrapper of the morphing container
transitionName: "layout"The navigate.push option
layoutIdThe 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. The layoutId you pass to navigate.push arrives on the destination as useScreen().layoutId. Tag the container, image, title, and price with a shared prefix and they morph as one unit.

Gallery.tsx
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>
  );
}
Photo.tsx
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 from useScreen().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.

On this page