Skip to content

cajaun/expo-router-forms-components

 
 

Repository files navigation

Common components for Expo apps

Components that I use in Expo Router apps that are generally optimized for iOS, dark mode, and servers. Main part is the forms which look like Apple's settings app. These should be replaced with proper SwiftUI/Jetpack Compose in the future, but it's still useful to have JS versions for platforms that don't have native support.

For best results, just copy the files to another project. Here are the other deps:

bunx expo install expo-haptics expo-symbols expo-blur expo-web-browser @bacons/apple-colors vaul @react-native-segmented-control/segmented-control

You can also just bootstrap a project with this repo:

bunx create-expo -t https://github.com/EvanBacon/expo-router-forms-components

Stack

Use the correct stack header settings for peak iOS defaults:

import Stack from "@/components/ui/Stack";
import ThemeProvider from "@/components/ui/ThemeProvider";

export default function Layout() {
  return (
    <ThemeProvider>
      <Stack
        screenOptions={{
          title: "🥓 Bacon",
        }}
      />
    </ThemeProvider>
  );
}

Use headerLargeTitle: true to get the large header title.

Use <Form.Link headerRight> to add a link to the right side of the header with correct hit size and padding for Forms. The default color will be system blue.

<Stack
  screenOptions={{
    title: "🥓 Bacon",
    headerRight: () => (
      <Form.Link headerRight href="/info">
        Info
      </Form.Link>
    ),
  }}
/>

This stack uses vaul on web to make modal look like a native modal.

Bottom sheet

Works on web too!

You can open routes as a bottom sheet on iOS:

<Stack.Screen name="info" sheet />

This sets custom options for React Native Screens:

{
  presentation: "formSheet",
  gestureDirection: "vertical",
  animation: "slide_from_bottom",
  sheetGrabberVisible: true,
  sheetInitialDetentIndex: 0,
  sheetAllowedDetents: [0.5, 1.0],
}
  • Use sheetAllowedDetents to change the snap points of the sheet.
  • Change the corder radius with sheetCornerRadius: 48.

Tabs

The custom tabs adds blurry backgrounds and haptics on iOS. You can also use the shortcut systemImage to set the icon.

import ThemeProvider from "@/components/ui/ThemeProvider";

import Tabs from "@/components/ui/Tabs";

export default function Layout() {
  return (
    <ThemeProvider>
      <Tabs>
        <Tabs.Screen name="(index)" systemImage="house.fill" title="Home" />
        <Tabs.Screen name="(info)" systemImage="brain.fill" title="Info" />
      </Tabs>
    </ThemeProvider>
  );
}

Forms

Start lists with a <Form.List> and add sections with <Form.Section>. Setting navigationTitle="Settings" will update the title of the stack header.

<Form.List navigationTitle="Settings">
  <Form.Section title="Developer">
    <Form.Link target="_blank" href="https://evanbacon.dev">
      Evan Bacon
    </Form.Link>
    <Form.Link href="https://evanbacon.dev">Evan Bacon in browser</Form.Link>
  </Form.Section>
</Form.List>
Internals

Form list is a wrapper around a scroll view with some extra styles and helpers.

<BodyScrollView
  contentContainerStyle={{
    padding: 16,
    gap: 24,
  }}
>
  <Form.Section title="Developer">
    <Form.Link target="_blank" href="https://evanbacon.dev">
      Evan Bacon
    </Form.Link>
    <Form.Link href="https://evanbacon.dev">Evan Bacon in browser</Form.Link>
  </Form.Section>
</BodyScrollView>

Form Sections

All top-level children will become items.

Add title and footer to a section. These can be strings or React nodes.

import * as AC from "@bacons/apple-colors";

<Form.Section
  title="Header"
  footer={
    <Text>
      Help improve Search by allowing Apple to store the searches you enter into
      Safari, Siri, and Spotlight in a way that is not linked to you.{"\n\n"}
      Searches include lookups of general knowledge, and requests to do things like
      play music and get directions.{"\n"}
      <Link style={{ color: AC.link }} href="/two">
        About Search & Privacy...
      </Link>
    </Text>
  }
>
  <Text>Default</Text>
</Form.Section>;

Form Items

  • Form.Text has extra types for hint and custom styles to have adaptive colors for dark mode. The font size is also larger to match the Apple defaults.
  • Adds the systemImage prop to append an SF Symbol icon before the text. The color of this icon will adopt the color of the text style.
<Form.Text>Hey</Form.Text>

Add a hint to the right-side of the form item:

<Form.Text hint="right">Left</Form.Text>

Add a custom press handler to the form item:

<Form.Text
  onPress={() => {
    console.log("Pressed");
  }}
>
  Press me
</Form.Text>

You can also use <Button /> from React Native similar to SwiftUI:

<Button title="Open" onPress={() => console.log("Pressed")} />

Form Link

Open with in-app browser using target="_blank" (only works when the href is an external URL):

<Form.Link target="_blank" href="https://evanbacon.dev">
  Evan Bacon
</Form.Link>

Add a hint to the right-side of the form item:

<Form.Link hint="123" href="/foo">
  Evan Bacon
</Form.Link>

Alternatively, use an HStack-type system instead of the hint hack:

<Form.HStack>
  <Form.Text>Foo</Form.Text>
  <View style={{ flex: 1 }} />
  <Form.Text style={Form.FormFont.secondary}>Bar</Form.Text>
</Form.HStack>

Add a quick icon before the text:

<Form.Link href="/two" systemImage="person.fill.badge.plus">
  Evan Bacon
</Form.Link>

Customize the color, size, etc:

<Form.Link
  href="/two"
  systemImage={{
    name: "person.fill.badge.plus",
    color: AC.systemBlue,
    size: 128,
  }}
>
  Evan Bacon
</Form.Link>

Hint and wrapping

Beautifully display a key/value pair with the hint="" property. This can also be created manually for extra customization.

Screenshot 2025-02-02 at 9 37 09 PM

The key here is to use flexShrink to support floating to the right, then wrapping correctly when the text gets too long.

Use flexWrap to position the text below the title when it gets too long instead of shifting the title down vertically.

<Form.Section title="Right text">
  <Form.Text hint="Long hint with extra content that should float below the content">
    Hint
  </Form.Text>

  {/* Custom */}
  <Form.HStack>
    <Form.Text>Opening</Form.Text>
    {/* Spacer */}
    <View style={{ flex: 1 }} />
    {/* Right */}
    <Form.Text style={{ flexShrink: 1, color: AC.secondaryLabel }}>
      Long list of text that should wrap around when it gets too long
    </Form.Text>
  </Form.HStack>

  {/* Custom with wrap-below */}
  <Form.HStack style={{ flexWrap: "wrap" }}>
    <Form.Text>Opening</Form.Text>
    {/* Spacer */}
    <View style={{ flex: 1 }} />
    {/* Right */}
    <Form.Text style={{ flexShrink: 1, color: AC.secondaryLabel }}>
      Long list of text that should wrap around when it gets too long
    </Form.Text>
  </Form.HStack>
</Form.Section>

Form Description and Item

Add a list item with an image and text + description combo:

<Form.HStack style={{ gap: 16 }}>
  <Image
    source={{ uri: "https://github.com/evanbacon.png" }}
    style={{
      aspectRatio: 1,
      height: 48,
      borderRadius: 999,
    }}
  />
  <View style={{ gap: 4 }}>
    <Form.Text>Evan's iPhone</Form.Text>
    <Form.Text style={Form.FormFont.caption}>This iPhone 16 Pro Max</Form.Text>
  </View>

  {/* Spacer */}
  <View style={{ flex: 1 }} />

  <IconSymbol color={AC.systemBlue} name="person.fill.badge.plus" size={24} />
</Form.HStack>

Create a linkable version like this:

<Form.Link href="/two">
  <View style={{ gap: 4 }}>
    <Form.Text>Evan's iPhone</Form.Text>
    <Form.Text style={Form.FormFont.caption}>This iPhone 16 Pro Max</Form.Text>
  </View>
</Form.Link>

List Style

The default listStyle is "auto" but you can access the old-style with "grouped":

<Form.List listStyle="grouped">
  <Form.Section title="Developer">
    <Form.Link target="_blank" href="https://evanbacon.dev">
      Evan Bacon
    </Form.Link>
  </Form.Section>
</Form.List>

Simulator Screenshot - iPhone 16 - 2025-02-02 at 22 12 25

Colors

Be sure to use @bacons/apple-colors for high-quality P3 colors.

Icons

Use the IconSymbol component to use Apple's SF Symbols.

Status Bar

Avoid using <StatusBar> on iOS as the system has built-in support for changing the color better than most custom solutions. Enable OS-changing with:

{
  "expo": {
    "userInterfaceStyle": "automatic",
    "ios": {
      "infoPlist": {
        "UIViewControllerBasedStatusBarAppearance": true,
      }
    }
  }
}

This won't work as expected in Expo Go. Use a dev client to understand the behavior better.

Segments

npx expo install @react-native-segmented-control/segmented-control

For tabbed content that doesn't belong in the router, use the Segment component:

import {
  Segments,
  SegmentsList,
  SegmentsContent,
  SegmentsTrigger,
} from "@/components/ui/Segments";

export default function Page() {
  return (
    <Segments defaultValue="account">
      <SegmentsList>
        <SegmentsTrigger value="account">Account</SegmentsTrigger>
        <SegmentsTrigger value="password">Password</SegmentsTrigger>
      </SegmentsList>

      <SegmentsContent value="account">
        <Text>Account Section</Text>
      </SegmentsContent>
      <SegmentsContent value="password">
        <Text>Password Section</Text>
      </SegmentsContent>
    </Segments>
  );
}

This can be used with React Server Components as the API is entirely declarative.

Toggle

Add a toggle switch item using hint and Switch from React Native:

<Form.Text hint={<Switch />}>Label</Form.Text>

You can also build the item manually for more customization:

<Form.HStack>
  <Form.Text>Label</Form.Text>
  <View style={{ flex: 1 }} />
  <Switch />
</Form.HStack>

Content Unavailable

Similar to SwiftUI's ContentUnavailableView.

Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2025-01-15.at.15.28.05.mp4

For empty states, use the <ContentUnavailable /> component.

There are three main uses:

  1. No search results: <ContentUnavailable search />. Use search as a string to show the invalid query <ContentUnavailable search="my query" />.
  2. No internet connection: <ContentUnavailable internet />. This shows an animated no connection screen.
  3. Everything else. Use title, description, and systemImage to customize the message. <ContentUnavailable title="No content" systemImage="car" description="Could not find car" />

Other info:

  • The systemImage can be the name of an SF Symbol or a React node. This is useful for custom/animated icons.
  • actions can be provided for a list of buttons to render under the content, e.g. <ContentUnavailable internet actions={<Button title="Refresh" />} />

About

Free UI components I use for building Expo Router apps

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 99.0%
  • Other 1.0%