Screens
@zynthjs/screens provides the low-level screen primitives used to render navigation stacks, tab content, and sheet-style flows in Zynth applications.
On iOS and Android, these primitives bridge to native containers and transition handling. @zynthjs/router uses them to render stack and tab navigators while keeping navigation state in JavaScript. On Web, the package provides a partial adapter built from DOM containers and CSS-based transitions.
Most applications use this package through @zynthjs/router, but the primitives are also available for custom navigation systems that need direct control over active screens, transition styles, and native lifecycle events.
Basic usage
Stack-style container
import { Screen, ScreenContainer } from "@zynthjs/screens";
export function App(props: { route: "home" | "details" }) {
return (
<ScreenContainer>
<Screen screenKey="home" active={props.route === "home"} animation="none">
<HomeScreen />
</Screen>
<Screen
screenKey="details"
active={props.route === "details"}
animation="push"
>
<DetailsScreen />
</Screen>
</ScreenContainer>
);
}
Router integration
@zynthjs/router renders stack screens through ScreenContainer and Screen, and tab content through ScreenTabsContainer.
import { NavigationContainer, createStackNavigator } from "@zynthjs/router";
type RootStackParams = {
Home: undefined;
Details: { itemId: string };
};
const Stack = createStackNavigator<RootStackParams>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "Overview" }}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={{ animation: "push", title: "Item" }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
Advanced examples
Native lifecycle events
Screen exposes native lifecycle callbacks for appearance and disappearance. These are useful when the screen primitive is used directly.
import { Screen, ScreenContainer } from "@zynthjs/screens";
export function Flow(props: { active: "feed" | "profile" }) {
return (
<ScreenContainer>
<Screen
screenKey="feed"
active={props.active === "feed"}
animation="none"
onDidAppear={() => {
console.log("Feed appeared");
}}
>
<FeedScreen />
</Screen>
<Screen
screenKey="profile"
active={props.active === "profile"}
animation="fade"
onWillDisappear={() => {
console.log("Profile will disappear");
}}
>
<ProfileScreen />
</Screen>
</ScreenContainer>
);
}
Covered screens for modal presentation
Use covered when a screen remains visible underneath a modal-style transition.
import { Screen, ScreenContainer } from "@zynthjs/screens";
export function ModalFlow(props: { step: "list" | "compose" }) {
const showingCompose = () => props.step === "compose";
return (
<ScreenContainer>
<Screen
screenKey="list"
active={true}
covered={showingCompose()}
animation="none"
>
<MessageListScreen />
</Screen>
<Screen screenKey="compose" active={showingCompose()} animation="modal">
<ComposeScreen />
</Screen>
</ScreenContainer>
);
}
Native header integration with @zynthjs/router
In stack navigation, router screen options are mapped to Screen header props. On iOS this drives the native navigation bar. On Android and Web, the router currently renders the stack header in JavaScript.
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{
title: "Profile",
largeTitle: true,
headerStyle: "liquidGlass",
headerTintColor: "#111827",
headerTitleColor: "#111827",
headerBackgroundColor: "#ffffff",
headerRightButton: {
title: "Edit",
style: "done",
onPress: () => {
console.log("Edit profile");
},
},
}}
/>
Native tab containers
ScreenTabsContainer manages the content area for tab navigation. @zynthjs/router uses it to preserve tab content and switch the selected tab index.
import { ScreenTabsContainer } from "@zynthjs/screens";
import { createSignal } from "solid-js";
export function TabsExample() {
const [selectedIndex, setSelectedIndex] = createSignal(0);
return (
<ScreenTabsContainer
selectedIndex={selectedIndex()}
tabBarOptions={{
activeTintColor: "#2563eb",
inactiveTintColor: "#6b7280",
showLabels: true,
}}
tabBarItems={[
{
key: "feed",
routeName: "Feed",
label: "Feed",
icon: { type: "descriptor", systemName: "newspaper" },
},
{
key: "settings",
routeName: "Settings",
label: "Settings",
icon: { type: "descriptor", systemName: "gearshape" },
},
]}
nativeTabBarEnabled={true}
onNativeTabSelect={setSelectedIndex}
>
<FeedScreen />
<SettingsScreen />
</ScreenTabsContainer>
);
}
Sheet-style navigation
ScreenSheetContainer is the container used by @zynthjs/router for iOS bottom-sheet flows. The router’s bottom-sheet navigator builds on this primitive together with @zynthjs/components bottom sheet presentation.
BottomSheet navigation is currently experimental and unstable.
import { Screen, ScreenSheetContainer } from "@zynthjs/screens";
export function SheetStack(props: { route: "filters" | "sort" }) {
return (
<ScreenSheetContainer>
<Screen
screenKey="filters"
active={props.route === "filters"}
animation="none"
>
<FiltersScreen />
</Screen>
<Screen
screenKey="sort"
active={props.route === "sort"}
animation="sheet-blur"
>
<SortScreen />
</Screen>
</ScreenSheetContainer>
);
}
Special cases and unusual features
- Router stack navigation uses
ScreenContainerandScreendirectly. The first route is rendered without animation, and later transitions are chosen from router screen options such asanimation,presentation, andanimationEnabled. - Router tab navigation does not wrap each tab in
Screen. It usesScreenTabsContainerto manage selected content while preserving route state and tab history. - Router bottom-sheet navigation uses
ScreenSheetContaineron iOS andScreenContaineron Android. BottomSheet navigation is currently experimental and unstable. gestureEnabledis wired to native back-swipe handling on iOS stack screens. When disabled, the interactive back gesture is disabled for that screen.headerOptionsare primarily a native iOS feature. Router maps stack screen options such astitle,subtitle,largeTitle,headerTintColor,headerBackgroundColor,headerBlurEffect,headerStyle,headerBackVisible, and native right-side accessories onto this API.coveredis relevant when a screen should remain visually behind another screen, such as modal presentation. Router computes this automatically for modal-style stack transitions.- Supported screen animation values are
push,modal,sheet-blur,zoom,fade, andnone. - On iOS, the native layer includes dedicated implementations for push, modal, blur-sheet, zoom, and fade transitions.
- On Android, push, modal, zoom, fade, and none are implemented natively. The package export includes
sheet-blur, but Android uses its standard native transition path for sheet-style flows. - Web support is partial.
Screen,ScreenContainer, andScreenTabsContainerare available with CSS-based transitions. Native headers, native tab bars, and sheet-specific containers do not have the same behavior as iOS and Android. - In direct usage,
screenKeyshould stay stable for the lifetime of a route. Router generates and maintains these keys automatically.
API reference
Components
ScreenContainer(props)
Container for stack-like screen flows.
style?: Stylechildren?: JSX.Element
ScreenSheetContainer(props)
Container for sheet-style screen flows.
style?: Stylechildren?: JSX.Element
ScreenTabsContainer(props)
Container for tab content and optional native tab bar metadata.
selectedIndex: numbertabAnimation?: ScreenAnimationTypetabBarOptions?: ScreenTabBarOptionstabBarItems?: ScreenTabBarItemDescriptor[]nativeTabBarEnabled?: booleanonNativeTabSelect?: (index: number) => voidonNativeTabMount?: (event: { surfaceId: number; routeKey: string; active: boolean }) => voidonNativeTabUpdate?: (event: { surfaceId: number; routeKey: string; active?: boolean }) => voidstyle?: Stylechildren?: JSX.Element
Screen(props)
Primitive representing a single screen in a container.
screenKey: stringactive: booleancovered?: booleananimation?: ScreenAnimationTypegestureEnabled?: booleanheaderOptions?: ScreenHeaderOptionsonNativeBack?: () => voidonNativeHeaderRightPress?: () => voidonWillAppear?: () => voidonDidAppear?: () => voidonWillDisappear?: () => voidonDidDisappear?: () => voidstyle?: Stylechildren?: JSX.Element
Types
ScreenAnimationType
"push""modal""sheet-blur""zoom""fade""none"
ScreenHeaderOptions
title?: stringsubtitle?: stringprefersLargeTitle?: booleanheaderStyle?: ScreenHeaderStylevisible?: booleanbackVisible?: booleantintColor?: stringtitleColor?: stringbackgroundColor?: stringtransparent?: booleanshadowVisible?: booleanblurEffect?: ScreenHeaderBlurEffectuserInterfaceStyle?: ScreenUserInterfaceStylerightButton?: ScreenHeaderButtonOptionsrightAccessory?: ScreenHeaderAccessoryDescriptor
ScreenHeaderStyle
"default""liquidGlass"
ScreenHeaderBlurEffect
"systemUltraThin""systemThin""systemChromatic"
ScreenUserInterfaceStyle
"dark""light""system"
ScreenHeaderButtonOptions
title?: stringstyle?: "plain" | "done" | "icon" | "prominent"systemItem?: "close"
ScreenTabBarOptions
visible?: booleanbackgroundColor?: stringactiveIndicatorColor?: stringactiveTintColor?: stringinactiveTintColor?: stringshowLabels?: booleanblurEffectStyle?: "systemUltraThinMaterial" | "systemThinMaterial" | "systemChromeMaterial" | "systemMaterial" | "none"
ScreenTabBarItemDescriptor
key: stringrouteName: stringlabel?: stringbadge?: string | numberbadgeColor?: stringhidden?: booleanicon?: ScreenTabBarIcon
ScreenTabBarIcon
Union of:
{ type: "descriptor"; systemName?: string; assetName?: string; uri?: string; glyph?: string; glyphFontFamily?: string; glyphFontSize?: number }{ type: "surface"; routeKey: string }
Router-related helper types
RouteDefinition<Params>
name: stringparams?: Params
NavigationState<ParamList>
routes: RouteDefinition<ParamList[keyof ParamList]>[]index: number