Core

Screen

Screen is what every route renders: the screen's surface, its top and bottom bars, and its safe-area insets.

1 min read
Inbox.tsx
import { Screen } from "@flemo/react";​export default function Inbox() {  return (    <Screen topBar={<TopBar title="Inbox" />} sharedBottomBar={<TabBar />}>      <MailList />    </Screen>  );}

Bars that move or stay

PropsRenderedWhen the screen changes
topBar / bottomBarInside the screen boxMoves with its screen, never stays still
sharedTopBar / sharedBottomBarBeside the screen boxStays in place if both screens have a matching bar

Use a shared bar for global UI like a tab bar, so it does not animate on every push.

Matching shared bars

Label bars with sharedTopBarId or sharedBottomBarId when one position serves different roles. Only bars with equal IDs stay in place during the transition.

tsx
<Screen sharedBottomBar={<TabBar />} sharedBottomBarId="main-tabs" />​<Screen sharedBottomBar={<BuilderActions />} sharedBottomBarId="pattern-builder-actions" />

Use a for UI that is identical on every screen. Use a shared bar when each screen has its own bar that should still look continuous.

Safe areas

Screen reserves the top and bottom safe areas itself. In a hybrid WebView, turn off native safe-area handling and let the web handle the insets.

tsx
<Screen  statusBarHeight="env(safe-area-inset-top)"  systemNavigationBarHeight="env(safe-area-inset-bottom)">  ...</Screen>

The safe-area strips then move and change color with the screen, instead of content sliding under static native bars.

Props

PropTypeDefault
topBar / bottomBarReactNodenone
sharedTopBar / sharedBottomBarReactNodenone
sharedTopBarId / sharedBottomBarIdstring | numbernone
backgroundColorstringwhite
statusBarHeight / statusBarColorstringnone
systemNavigationBarHeight / systemNavigationBarColorstringnone
hideStatusBar / hideSystemNavigationBarbooleanfalse
contentScrollablebooleantrue

A moving screen has a transform, so even position: fixed children are trapped in it. Put overlays that must cover the bars in a .

How it worksDetails

Shared bar matching

  • Two bars stay in place only when their IDs are equal. Otherwise each bar enters and leaves with its own screen.
  • Two unlabelled bars still match by position, the legacy behavior.
  • A labelled bar never matches an unlabelled one.

Props in detail

  • statusBarHeight and systemNavigationBarHeight reserve space above the top bar and below the bottom bar. The *Color props fill those areas, and hide* removes them for one screen.
  • backgroundColor is the screen's own surface. flemo checks the computed color. When it is verifiably opaque, the other screen in the transition can wait at its end position, hidden behind this one, before the motion starts. Otherwise it waits paused at its start. A transparent screen gives that up.
  • contentScrollable={false} scrolls the whole screen box, bars included, instead of only the content area.
  • A screen also creates a stacking context while it moves. Wrap elements that move on their own inside it in a .
  • Screen also accepts ordinary div props, except the pointer handlers (onPointerDown, onPointerMove, onPointerUp, onPointerCancel).

Content and the transition

A newly mounting screen renders its children in the same commit as the screen box, and the transition starts from the first frame where they are drawn. What slides in is your real content, never an empty shell. A heavy first render delays the start, but the animation always plays in full. It is never cut short or skipped.

Covered screens

A covered screen stays mounted and keeps its DOM state, such as scroll position and form values. flemo stops drawing it at once and hides it with React's Activity, sometimes after a short delay, so its effects unmount while covered and mount again when a pop reveals it.

Edit on GitHub