expo-glass-tabs: Liquid Glass Tabs for Expo Router

Description:

expo-glass-tabs is a React Native tab-bar component for Expo Router that adds a floating liquid-glass bar with minimize-on-scroll, sliding selection, finger scrubbing, and progressive edge blur.

It uses Expo Router headless tabs for route structure, Reanimated worklets for motion, and native glass materials when iOS 26 provides them.

Features

  • Renders a floating tab bar through Expo Router headless tabs.
  • Shrinks the pill and collapses labels as the user scrolls down.
  • Slides the active highlight with an interruptible spring.
  • Tracks finger scrubbing and fires haptic ticks at tab boundaries on iOS.
  • Uses iOS liquid glass materials with a solid fallback on older iOS versions and Android.
  • Adds progressive blur at the top or bottom edge of a screen.
  • Animates focused tab screens with a fade and a small scale change.
  • Accepts SF Symbols or custom tinted React Native icons.

How To Use It

Install the Expo Dependencies

Install the package and the Expo Router tab dependencies with the command from the project README:

npx expo install expo-glass-tabs expo-blur expo-glass-effect expo-haptics expo-symbols react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-screens

Wrap the application root in GestureHandlerRootView.

// app/_layout.tsx
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Slot } from "expo-router";
export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Slot />
    </GestureHandlerRootView>
  );
}

Build the Headless Tab Layout

Create a headless Expo Router tab layout and render the glass bar through TabList asChild.

The GlassTabItem object can use an SF Symbol name through icon. The GlassTabButton receives the item and renders the active and inactive states.

// app/(tabs)/_layout.tsx
import { useRouter } from "expo-router";
import {
  Tabs,
  TabList,
  TabSlot,
  TabTrigger,
} from "expo-router/ui";
import {
  GlassTabBar,
  GlassTabButton,
  TabBarMinimizeProvider,
  renderFadingTabScreen,
  type GlassTabItem,
} from "expo-glass-tabs";
const tabItems: (GlassTabItem & { href: string })[] = [
  { name: "index", href: "/", label: "Home", icon: "house.fill" },
  { name: "market", href: "/market", label: "Market", icon: "chart.bar.fill" },
  { name: "wallet", href: "/wallet", label: "Wallet", icon: "wallet.pass.fill" },
  { name: "alerts", href: "/alerts", label: "Alerts", icon: "bell.fill" },
];
export default function TabLayout() {
  const router = useRouter();
  return (
    <TabBarMinimizeProvider>
      <Tabs>
        {/* Fade and scale the active route during tab changes. */}
        <TabSlot style={{ height: "100%" }} renderFn={renderFadingTabScreen} />
        <TabList asChild>
          <GlassTabBar
            onIndexSelected={(index) =>
              router.navigate(tabItems[index].href as never)
            }
          >
            {tabItems.map(({ href, ...item }, index) => (
              <TabTrigger key={item.name} name={item.name} href={href as never} asChild>
                <GlassTabButton item={item} index={index} />
              </TabTrigger>
            ))}
          </GlassTabBar>
        </TabList>
      </Tabs>
    </TabBarMinimizeProvider>
  );
}

Minimize the Bar From Scroll Events

Attach useMinimizeOnScroll to an animated scroll view inside a tab screen. Set scrollEventThrottle to 16 so the shared progress value responds to regular scroll events.

The hook updates the tab bar’s shared minimize progress from scroll direction. The provider exposes progress 0 for the expanded state and 1 for the minimized state.

// app/(tabs)/market.tsx
import Animated from "react-native-reanimated";
import { useMinimizeOnScroll } from "expo-glass-tabs";
export default function MarketScreen() {
  const onScroll = useMinimizeOnScroll();
  return (
    <Animated.ScrollView
      onScroll={onScroll}
      scrollEventThrottle={16}
      contentContainerStyle={{ padding: 20, paddingBottom: 140 }}
    >
      {/* Screen content goes here. */}
    </Animated.ScrollView>
  );
}

Add a Custom Icon

Use renderIcon when an SF Symbol does not match the product’s icon set. The renderer receives the current tint and icon size.

import { Image } from "react-native";
import {
  GlassTabButton,
  type GlassTabItem,
} from "expo-glass-tabs";
const accountItem: GlassTabItem = {
  name: "account",
  label: "Account",
  renderIcon: ({ tint, size }) => (
    <Image
      source={require("./assets/account-mark.png")}
      style={{ width: size, height: size, tintColor: tint }}
    />
  ),
};
export function AccountButton() {
  return <GlassTabButton item={accountItem} />;
}

Customize the Theme

Pass a partial GlassTabBarTheme object to set the active tint, inactive tint, highlight, glass tint, and solid fallback colors.

<GlassTabBar
  theme={{
    activeTint: "#FFFFFF",
    inactiveTint: "#A1A1AA",
    highlight: "rgba(255, 255, 255, 0.14)",
    glassTint: "rgba(10, 10, 12, 0.55)",
    solidFallback: "rgba(18, 18, 20, 0.94)",
  }}
  haptics
>
  {/* Tab buttons */}
</GlassTabBar>

Add Progressive Edge Blur

Use ProgressiveBlur as a separate screen layer when a page needs a soft transition at its top or bottom edge.

import { ProgressiveBlur } from "expo-glass-tabs";
export function TopEdgeBlur() {
  return (
    <ProgressiveBlur
      direction="top"
      intensity={6}
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        height: 144,
      }}
    />
  );
}

API Reference

Exported Components and Hooks

ExportPurpose
GlassTabBarRenders the themed tab bar and handles selection callbacks.
GlassTabButtonRenders one tab item with its active and inactive states.
TabBarMinimizeProviderStores the shared progress used by the tab bar minimization animation.
useMinimizeOnScrollReturns an animated scroll handler for scroll-driven minimization.
useTabBarMinimizedReads the tab bar minimize shared value.
renderFadingTabScreenProvides the Expo Router TabSlot render function for fade and scale transitions.
ProgressiveBlurRenders a stacked blur and gradient edge layer.
MINIMIZE_SPRINGExports the default spring configuration.

Tab Items and Theme

GlassTabItem has these fields:

  • name: string identifies the route tab.
  • label: string supplies the visible tab label.
  • icon?: SymbolViewProps["name"] selects an Expo Symbol.
  • renderIcon?: ({ tint: string; size: number }) => ReactNode renders a custom icon.

GlassTabBarProps extends Expo Router’s TabListProps and adds:

  • onIndexSelected?: (index: number) => void reports the selected tab index.
  • theme?: Partial<GlassTabBarTheme> overrides the five theme colors.
  • haptics?: boolean controls boundary haptic ticks and defaults to true.

GlassTabBarTheme contains activeTint, inactiveTint, highlight, glassTint, and solidFallback.

ProgressiveBlur accepts React Native ViewProps plus:

  • intensity?: number, which defaults to 5.
  • direction?: "top" | "bottom", which defaults to "top".

Alternatives

FAQs

Q: Why does the tab bar stay expanded?

A: Place TabBarMinimizeProvider around the tab layout and attach useMinimizeOnScroll to the screen’s Animated.ScrollView.

Q: Why do taps or finger scrubbing fail?

A: Wrap the application root with GestureHandlerRootView, because Expo Router does not add that wrapper.

Q: What does the bar render on Android or older iOS versions?

A: It uses the configured solidFallback color because the iOS 26 UIGlassEffect material is not available there.

Q: How can I use a custom logo icon?

A: Add renderIcon to a GlassTabItem. The renderer receives the current tint and size.

Tags:

Add Comment