npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@esaart/glass-bottom-surface

v0.1.1

Published

Reusable SwiftUI glass bottom surface for Expo apps.

Downloads

335

Readme

Glass Bottom Surface

Reusable iOS SwiftUI bottom surface for Expo apps. It renders one native view that can morph between:

  • tab bar
  • contextual toolbar
  • status / toast capsule
  • optional accessory row above the bar

The package is intentionally product-agnostic. Keep routing, analytics, stores, permissions, generation state, and action handling outside this package.

For a full app-level architecture similar to Esaart's unified bottom surface, see docs/app-orchestration.md inside the published package.

Requirements

  • Expo SDK 56+
  • @expo/ui
  • iOS custom build / dev client
  • iOS 18+ deployment target

The native view uses iOS 26 Liquid Glass APIs when available and falls back to material backgrounds on older iOS versions.

Install

Install the package, then rebuild the native app:

npm install @esaart/glass-bottom-surface
npx pod-install ios

Because this is a native Expo module, the first install and every Swift change requires a new dev client/TestFlight/App Store build. JS-only usage changes can ship over the air after the native module is already present in the installed binary.

Test Without Publishing

From this repository:

npm pack --workspace=@esaart/glass-bottom-surface

In another Expo app:

npm install /path/to/esaart-glass-bottom-surface-0.1.1.tgz
npx pod-install ios

Workspace Usage

In a monorepo, add the package to the app:

{
  "dependencies": {
    "@esaart/glass-bottom-surface": "*"
  }
}

If Metro does not resolve workspace packages automatically, add an alias:

const path = require("path");

const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, "../..");
const packagesPath = path.join(monorepoRoot, "packages");

config.watchFolders = [...(config.watchFolders || []), packagesPath];
config.resolver.extraNodeModules = {
  ...config.resolver.extraNodeModules,
  "@esaart/glass-bottom-surface": path.join(packagesPath, "glass-bottom-surface"),
};

Basic Tabs

GlassBottomSurface is controlled from React. You own the selected index and route switching.

import * as React from "react";
import { StyleSheet, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { GlassBottomSurface } from "@esaart/glass-bottom-surface";

export function AppBottomBar() {
  const insets = useSafeAreaInsets();
  const [selection, setSelection] = React.useState(0);

  return (
    <View
      pointerEvents="box-none"
      style={[
        StyleSheet.absoluteFill,
        { justifyContent: "flex-end", paddingBottom: insets.bottom },
      ]}
    >
      <GlassBottomSurface
        style={{ height: 78 }}
        icons={["house.fill", "square.stack.3d.up.fill", "bubble.left.fill"]}
        labels={["Home", "Projects", "Chat"]}
        selection={selection}
        mainActionIcon="magnifyingglass"
        mainActionShape="circle"
        mainActionTint="#FF7A1A"
        onSelectionChange={setSelection}
        onTabPress={(index) => {
          setSelection(index);
          // Navigate to the selected route here.
        }}
        onMainAction={() => {
          // Open search, compose, create, etc.
        }}
      />
    </View>
  );
}

Toolbar Mode

Set isMorphed and pass toolbarItems. The same native surface morphs from tabs into toolbar controls.

import {
  GlassBottomSurface,
  type GlassToolbarItem,
} from "@esaart/glass-bottom-surface";

const flowToolbarItems: GlassToolbarItem[] = [
  { type: "button", id: "lock", icon: "lock.open", label: "Lock" },
  { type: "button", id: "assistant", icon: "sparkles", label: "Assistant" },
  { type: "spacer" },
  {
    type: "button",
    id: "undo",
    icon: "arrow.uturn.backward",
    label: "Undo",
    disabled: true,
  },
  { type: "button", id: "redo", icon: "arrow.uturn.forward", label: "Redo" },
  { type: "button", id: "layout", icon: "square.grid.2x2", label: "Layout" },
  { type: "button", id: "add", icon: "plus", label: "Add" },
  { type: "main", label: "Run" },
];

<GlassBottomSurface
  style={{ height: 78 }}
  icons={["house.fill", "square.stack.3d.up.fill", "bubble.left.fill"]}
  labels={["Home", "Projects", "Chat"]}
  selection={0}
  isMorphed
  flexibility="fixed"
  toolbarItems={flowToolbarItems}
  mainActionIcon="play.fill"
  mainActionShape="circle"
  mainActionTint="#FF7A1A"
  onToolbarAction={(id) => {
    switch (id) {
      case "lock":
        break;
      case "assistant":
        break;
      case "undo":
        break;
      case "redo":
        break;
      case "layout":
        break;
      case "add":
        break;
    }
  }}
  onMainAction={() => {
    // Run primary action.
  }}
/>;

Flexible Toolbar Layout

Use flexibility="flexible" with { type: "flex" } to push groups apart. This is useful for selection mode or toolbars with leading and trailing groups.

const selectionToolbarItems: GlassToolbarItem[] = [
  { type: "button", id: "close", icon: "xmark", label: "Close" },
  { type: "button", id: "count", icon: "checkmark.circle", title: "3" },
  { type: "flex" },
  { type: "button", id: "share", icon: "square.and.arrow.up", label: "Share" },
  { type: "button", id: "delete", icon: "trash", label: "Delete" },
];

<GlassBottomSurface
  style={{ height: 78 }}
  icons={["house.fill"]}
  selection={0}
  isMorphed
  flexibility="flexible"
  toolbarItems={selectionToolbarItems}
  mainActionIcon="trash.fill"
  mainActionTint="#FF453A"
  onToolbarAction={(id) => {
    // Handle selection toolbar action.
  }}
/>;

Toolbar Item Types

| Type | Purpose | | --- | --- | | button | Icon button. Emits onToolbarAction(id). | | menu | Icon button with native menu actions. Emits selected action id. | | main | Primary circular/capsule action. Uses onMainAction. | | view | Native variant picker slot. Uses toolbarVariantCount. | | spacer | Starts a separate visual glass group. | | flex | Inserts flexible space between groups. |

Buttons

{ type: "button", id: "duplicate", icon: "doc.on.doc", label: "Duplicate" }

label is semantic metadata for your JS side. It is not shown on the native button. Use title only when you intentionally want visible text inside the toolbar item.

Menus

const items: GlassToolbarItem[] = [
  {
    type: "menu",
    id: "more",
    icon: "ellipsis",
    label: "More",
    actions: [
      { id: "download", icon: "arrow.down", title: "Download" },
      { id: "delete", icon: "trash", title: "Delete" },
    ],
  },
];

Variant Picker

const items: GlassToolbarItem[] = [
  { type: "view", kind: "variants", id: "view-mode" },
  { type: "main", label: "Run" },
];

<GlassBottomSurface
  icons={["house.fill"]}
  selection={0}
  isMorphed
  toolbarItems={items}
  toolbarVariantCount={3}
  toolbarSelectedVariant={1}
  onToolbarVariantSelect={(index) => {
    // Switch native variant index.
  }}
/>;

Status / Toast Mode

Set mode-style status props and isMorphed. The package does not own timers; your app decides when to show and hide the status state.

<GlassBottomSurface
  style={{ height: 78 }}
  icons={["house.fill"]}
  selection={0}
  isMorphed
  statusTitle="Saved"
  statusMessage="Project changes synced"
  statusIcon="checkmark.circle.fill"
  statusTint="#34C759"
/>;

For long text, the native view keeps the message constrained inside the surface. Prefer short titles and one short sentence for status messages.

Accessory Row

Use an accessory when you need persistent content above the bottom bar, such as mini player state, auth state, or long-running flow state.

<GlassBottomSurface
  style={{ height: 144 }}
  icons={["house.fill", "bubble.left.fill"]}
  labels={["Home", "Chat"]}
  selection={0}
  accessoryKind="flowRun"
  accessoryTitle="Generating"
  accessorySubtitle="Running 4 nodes"
  accessoryIcon="arrow.triangle.2.circlepath"
  accessoryTint="#FF7A1A"
  accessoryProgress={0.35}
  accessoryShowsProgress
  accessoryPrimaryActionIcon="xmark"
  accessorySecondaryActionIcon="chevron.up"
  onAccessoryAction={(id) => {
    // Native ids are emitted by the accessory controls.
  }}
/>;

When an accessory is visible, give the native view enough height for both the accessory and the bottom bar. In this project the app-level wrapper uses:

const dockHeight = 78;
const accessoryHeight = 58;
const accessoryGap = 8;
const totalHeight = dockHeight + accessoryHeight + accessoryGap;

Props Reference

Core

| Prop | Type | Description | | --- | --- | --- | | icons | string[] | SF Symbol names for tabs. | | labels | string[] | Optional tab labels. | | selection | number | Controlled selected tab index. | | isMorphed | boolean | Morphs from tabs into toolbar/status layouts. | | flexibility | "fixed" | "flexible" | Layout behavior for toolbar groups. | | style | StyleProp<ViewStyle> | React Native style for the host view. Set height here. |

Main / Side Actions

| Prop | Type | Description | | --- | --- | --- | | mainActionIcon | string | SF Symbol for the primary action. | | mainActionShape | "capsule" | "circle" | Primary action shape. | | mainActionTint | string | Primary tint color. | | mainActionForeground | string | Primary icon/text color. | | leadingActionIcon | string | Optional leading action icon. | | trailingActionIcon | string | Optional trailing action icon. |

Toolbar

| Prop | Type | Description | | --- | --- | --- | | toolbarItems | GlassToolbarItem[] | Preferred high-level toolbar config. | | toolbarVariantCount | number | Number of native variant options. | | toolbarSelectedVariant | number | Selected native variant index. |

Low-level toolbarItemIds, toolbarItemKinds, and related array props are exported for advanced callers, but most apps should use toolbarItems.

Status

| Prop | Type | Description | | --- | --- | --- | | statusTitle | string | Status title. | | statusMessage | string | Status detail text. | | statusIcon | string | SF Symbol for status. | | statusTint | string | Status accent color. |

Accessory

| Prop | Type | Description | | --- | --- | --- | | accessoryKind | "none" | string | Non-"none" value shows accessory. | | accessoryTitle | string | Accessory title. | | accessorySubtitle | string | Accessory subtitle. | | accessoryIcon | string | SF Symbol for accessory. | | accessoryTint | string | Accessory accent color. | | accessoryThumbnailUrl | string | Optional thumbnail image URL. | | accessoryProgress | number | Progress value from 0 to 1. | | accessoryShowsProgress | boolean | Shows progress line. | | accessoryPrimaryActionIcon | string | Primary accessory action icon. | | accessorySecondaryActionIcon | string | Secondary accessory action icon. |

Events

| Event | Payload | | --- | --- | | onSelectionChange | (selection: number) => void | | onTabPress | (selection: number) => void | | onMainAction | () => void | | onLeadingAction | () => void | | onTrailingAction | () => void | | onToolbarAction | (id: string) => void | | onToolbarVariantSelect | (index: number) => void | | onAccessoryAction | (id: string) => void |

Recommended App Wrapper

For a production app, wrap the package in your own app-level controller. The package should stay generic; your wrapper should know about routes, auth, media state, current flow, selection mode, toast timers, and analytics.

export function BottomSurfaceDock({ state }: { state: BottomSurfaceState }) {
  const insets = useSafeAreaInsets();
  const hasAccessory = state.accessoryKind !== "none";
  const height = hasAccessory ? 144 : 78;

  return (
    <View
      pointerEvents="box-none"
      style={{
        position: "absolute",
        left: 0,
        right: 0,
        bottom: insets.bottom,
        height,
      }}
    >
      <GlassBottomSurface
        style={{ height }}
        icons={state.tabs.map((tab) => tab.icon)}
        labels={state.tabs.map((tab) => tab.label)}
        selection={state.selection}
        isMorphed={state.mode !== "tabs"}
        toolbarItems={state.toolbarItems}
        accessoryKind={state.accessoryKind}
        accessoryTitle={state.accessoryTitle}
        accessorySubtitle={state.accessorySubtitle}
      />
    </View>
  );
}

Troubleshooting

The view is blank

The native module is not in the installed binary. Rebuild the dev client/custom build after installing the package:

npx pod-install ios
npx expo run:ios

Red screen: SwiftUI view mounted inside a standard UIView

Wrap the native SwiftUI component with the package wrapper. The package already uses Host from @expo/ui/swift-ui; do not call requireNativeView directly from app code unless you also provide the correct SwiftUI host.

CocoaPods cannot find the pod

Check that expo-module.config.json is next to package.json, then run:

npm install
npx pod-install ios

In a monorepo, make sure the app depends on the package and Metro/CocoaPods can resolve the workspace package.

Toolbar text appears when only an icon should show

Use label for semantic metadata and title only for intentionally visible text.

{ type: "button", id: "issue", icon: "exclamationmark.triangle", label: "Issue" }

Touches are blocked behind the surface

Put your absolute-positioned wrapper on pointerEvents="box-none" and let the native surface receive touches only within its own frame.