Screen

A screen's frame — pinned chrome, a content region, and whatever scrolls between them.

Open Source

Installation

Screen ships with the library — install it once, then import:

import { Screen } from "@delacour/react-native-ui/screen";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add screen

Usage

app/index.tsx
import { Button } from "@/components/ui/button";
import { Screen } from "@/components/ui/screen";
import { Text } from "@/components/ui/text";
import { useRouter } from "expo-router";

export default function Example() {
  const router = useRouter();

  function save() {
    console.log("saved");
  }

  return (
    <Screen>
      <Screen.Navbar>
        <Screen.Navbar.BackButton onPress={router.back} />
        <Screen.Navbar.Title>Settings</Screen.Navbar.Title>
      </Screen.Navbar>

      <Screen.ScrollArea contentContainerClassName="gap-4 p-4">
        <Text.Header>Notifications</Text.Header>
        <Text.Paragraph>Choose which alerts reach this device.</Text.Paragraph>
      </Screen.ScrollArea>

      <Screen.Footer>
        <Button onPress={save}>Save</Button>
      </Screen.Footer>
    </Screen>
  );
}

The navbar and footer measure themselves into a Reanimated context and every scrollable reserves exactly that, so no screen carries a hand-tuned padding number.

Placement

overlay floats the chrome over the content, which insets itself to match; static puts it in the flow and paints an opaque surface.

<Screen.Navbar placement="static">…</Screen.Navbar>
<Screen.Footer placement="overlay">…</Screen.Footer>

Borders

Both hairlines are drawn at rest; fadeBorderOnScroll opts into a scroll-linked ramp instead — the navbar's brightens as you leave the top, the footer's fades out as the content runs out.

<Screen.Navbar fadeBorderOnScroll>…</Screen.Navbar>
<Screen.Footer fadeBorderOnScroll>…</Screen.Footer>

Chat lists

Screen.ChatList is a discriminated union on variant: legend takes chronological oldest-first data and LegendList's own renderItem, flat is an inverted FlatList with newest-first data and React Native's.

<Screen.ChatList
  composerBaseHeight={56}
  data={messages}
  renderItem={renderMessage}
  variant="legend"
/>

Seed a chat list with composerBaseHeight

The footer publishes its real height a commit or two after the list's first layout, and the list has already scrolled to the end by then. Without the seed the newest message hides under the composer — intermittently, which is the worst kind of wrong.

Loading and error

Whole-screen states, each with its own navbar so a back gesture still works.

Loading and error
Loading and error
<Screen.Loading title="Loading" />

<Screen.Error message="That project no longer exists." onBack={router.back} title="Not found">
  <Button onPress={retry}>Try again</Button>
</Screen.Error>

API Reference

Requirements

The app must mount DelacourProvider at its root. Screen reads the safe-area and keyboard providers, and <KeyboardStateSync /> is what keeps the keyboard's shared values honest between screens — without it a screen renders "keyboard open" over a keyboard that is not there.

@legendapp/list is an optional peer, so an app that never imports Screen never resolves it.

Parts

A pushed screen
PartWhat it is
Screen.NavbarPinned top chrome. Compound: .Title, .Subtitle, .BackButton
Screen.ContentThe content region between the chrome
Screen.ViewA non-scrolling body
Screen.HeaderA scrolling header inside the content region
Screen.FooterPinned bottom chrome, keyboard-aware
Screen.ScrollAreaA ScrollView with the reserves applied
Screen.FlatList / .SectionList / .LegendListVirtualised lists, same reserves
Screen.ChatListAn inverted or oldest-first chat list
Screen.Loading / Screen.ErrorWhole-screen states

Screen

The root. It renders a View and passes ViewProps through.

Prop

Type

Shared shapes

Three prop shapes are shared between parts rather than restated, which is why the tables below are short.

Prop

Type

Screen.Navbar

Pinned chrome at the top. Publishes its measured height, which is what everything below it clears.

Prop

Type

Screen.Footer

Pinned chrome at the bottom, and the part that rides the keyboard.

Prop

Type

Screen.Content / Screen.Header / Screen.View

Content and Header take ScreenInsetProps; View is a plain ViewProps box that clears the chrome without scrolling.

Prop

Type

The scrollables

Screen.ScrollArea, Screen.FlatList, Screen.SectionList, Screen.LegendList and Screen.ChatList each extend their underlying list's own props plus ScreenScrollableProps. They insert the navbar and footer spacers for you, so content clears the chrome without a manual contentContainerStyle.

Prop

Type

Screen.Error / Screen.Loading

Whole-screen states, each with its own navbar so a back gesture still works.

Prop

Type

Screen.NavbarBackButton

A Pressable, so the vocabulary is inherited. onPress is required — this library takes no navigation dependency, so the app wires router.back() itself. There is no Navbar.Action either: a navbar action is <Button size="icon-sm" variant="secondary">.

Prop

Type

Screen.NavbarTitle / NavbarSubtitle / NavbarBackground / FooterBackground

The two text parts are Text presets and take its props. The two background parts take ViewProps and exist for a surface that has to sit behind the chrome rather than inside it — reach for backgroundClassName on the navbar first.

Prop

Type

On this page