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-screensWrap 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
| Export | Purpose |
|---|---|
GlassTabBar | Renders the themed tab bar and handles selection callbacks. |
GlassTabButton | Renders one tab item with its active and inactive states. |
TabBarMinimizeProvider | Stores the shared progress used by the tab bar minimization animation. |
useMinimizeOnScroll | Returns an animated scroll handler for scroll-driven minimization. |
useTabBarMinimized | Reads the tab bar minimize shared value. |
renderFadingTabScreen | Provides the Expo Router TabSlot render function for fade and scale transitions. |
ProgressiveBlur | Renders a stacked blur and gradient edge layer. |
MINIMIZE_SPRING | Exports the default spring configuration. |
Tab Items and Theme
GlassTabItem has these fields:
name: stringidentifies the route tab.label: stringsupplies the visible tab label.icon?: SymbolViewProps["name"]selects an Expo Symbol.renderIcon?: ({ tint: string; size: number }) => ReactNoderenders a custom icon.
GlassTabBarProps extends Expo Router’s TabListProps and adds:
onIndexSelected?: (index: number) => voidreports the selected tab index.theme?: Partial<GlassTabBarTheme>overrides the five theme colors.haptics?: booleancontrols boundary haptic ticks and defaults totrue.
GlassTabBarTheme contains activeTint, inactiveTint, highlight, glassTint, and solidFallback.
ProgressiveBlur accepts React Native ViewProps plus:
intensity?: number, which defaults to5.direction?: "top" | "bottom", which defaults to"top".
Alternatives
- Glassmorphic Pill Tab UI for React Native – Linear Tab Bar
- React Native Bottom Tabs with Native Components
- React Native Collapsible Header Tabs Component
- Expo Router headless tabs
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.





