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

expo-liquid-glass-tabs

v0.3.0

Published

Apple iOS 26 Liquid Glass tab bars for Expo & React Native — the real native bar on iOS and a gesture-driven custom bar (elastic pill, Telegram-style tint) on Android.

Readme

expo-liquid-glass-tabs

Apple iOS 26 Liquid Glass tab bars for Expo & React Native — the real native system tab bar on iOS, and a gesture-driven custom bar (elastic pill, Telegram-style icon tint, scroll-to-minimize) that looks the part on Android too.

npm install expo-liquid-glass-tabs

Demo

A demo recording is coming soon. To try it now, run the example app: cd example && npx expo run:ios — press-and-hold the pill and drag between tabs, and tap to see icons animate.

Features

  • Real iOS 26 Liquid Glass via Apple's own system tab bar (Expo Router NativeTabs)
  • Gesture-driven custom bar — tap to switch, or press-and-hold and drag the pill between tabs with an elastic stretch-and-settle, crossing haptics, and a glass surface
  • Telegram-style tint that tracks the pill
  • Scroll-to-minimize (shrinks into a compact pill as you scroll)
  • Any icons (render-prop), any number of tabs (even dynamic), light & dark out of the box

Install

npm install expo-liquid-glass-tabs
# peer dependencies (Expo):
npx expo install react-native-reanimated react-native-gesture-handler react-native-worklets \
  expo-blur expo-linear-gradient expo-glass-effect expo-haptics \
  @react-native-masked-view/masked-view react-native-safe-area-context
# expo-router is only needed for NativeLiquidGlassTabBar / LiquidGlassTabs

Wrap your app root in GestureHandlerRootView (required for the gesture bar):

import { GestureHandlerRootView } from 'react-native-gesture-handler';
// <GestureHandlerRootView style={{ flex: 1 }}>...</GestureHandlerRootView>

Which component?

| Component | Import from | What it is | Needs Expo Router | |---|---|---|---| | LiquidGlassTabBar | expo-liquid-glass-tabs | Controlled custom bar (gesture pill + tint + scroll-shrink). Works with any navigator (React Navigation, etc.) or standalone. | No | | NativeLiquidGlassTabBar | expo-liquid-glass-tabs/router | Apple's real iOS 26 system Liquid Glass tab bar | Yes | | LiquidGlassTabs | expo-liquid-glass-tabs/router | Auto-switch: native bar on iOS, custom bar on Android | Yes |

Why two entry points? The main entry is framework-agnostic and never imports expo-router, so apps without it (React Navigation, plain RN) bundle cleanly. The Expo-Router-only components live under expo-liquid-glass-tabs/router.

Migrating to 0.3.0: NativeLiquidGlassTabBar and LiquidGlassTabs moved from the main entry to expo-liquid-glass-tabs/router. Update those imports: import { NativeLiquidGlassTabBar, LiquidGlassTabs } from 'expo-liquid-glass-tabs/router'; LiquidGlassTabBar and the rest are unchanged.

Usage

LiquidGlassTabBar (controlled, works anywhere)

import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons';
import { useSharedValue } from 'react-native-reanimated';
import { LiquidGlassTabBar } from 'expo-liquid-glass-tabs';

const TABS = [
  { key: 'home',   icon: (on, c) => <Ionicons name={on ? 'home' : 'home-outline'} size={24} color={c} /> },
  { key: 'search', icon: (on, c) => <Ionicons name={on ? 'search' : 'search-outline'} size={24} color={c} /> },
  { key: 'me',     icon: (on, c) => <Ionicons name={on ? 'person' : 'person-outline'} size={24} color={c} /> },
];

function Bar() {
  const [active, setActive] = useState('home');
  const scrollY = useSharedValue(0); // optional — enables scroll-to-minimize
  return <LiquidGlassTabBar tabs={TABS} activeKey={active} onChange={setActive} scrollY={scrollY} />;
}

With React Navigation (bottom tabs)

LiquidGlassTabBar is navigation-agnostic — drop it in as the tabBar of @react-navigation/bottom-tabs. No expo-router needed (import from the main entry).

import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { LiquidGlassTabBar } from 'expo-liquid-glass-tabs';

const ICONS: Record<string, [keyof typeof Ionicons.glyphMap, keyof typeof Ionicons.glyphMap]> = {
  Chats:    ['chatbubble', 'chatbubble-outline'],
  Calls:    ['call', 'call-outline'],
  Settings: ['settings', 'settings-outline'],
};

function LiquidGlassTabBarAdapter({ state, navigation }: BottomTabBarProps) {
  const insets = useSafeAreaInsets();
  const tabs = state.routes.map((r) => ({
    key: r.name,
    icon: (on: boolean, c: string) => {
      const [active, inactive] = ICONS[r.name];
      return <Ionicons name={on ? active : inactive} size={24} color={c} />;
    },
  }));
  return (
    <LiquidGlassTabBar
      tabs={tabs}
      activeKey={state.routes[state.index].name}
      onChange={(name) => navigation.navigate(name)}
      bottomInset={insets.bottom}
    />
  );
}

// MainTabs.tsx
// <Tab.Navigator screenOptions={{ headerShown: false }}
//   tabBar={(props) => <LiquidGlassTabBarAdapter {...props} />}>
//   <Tab.Screen name="Chats" ... />
// </Tab.Navigator>

Animated icons (selected-state animations)

The icon render-prop receives (active, color, progress). There are two ways to animate the icon when its tab is selected — like Telegram's icons that react on tap:

1. One-shot on select — trigger an animation when active flips (great for Lottie, e.g. a phone that "rings" on select):

import LottieView from 'lottie-react-native';
import { useRef, useEffect } from 'react';

function RingingPhone({ active, color }: { active: boolean; color: string }) {
  const ref = useRef<LottieView>(null);
  useEffect(() => { if (active) ref.current?.play(); }, [active]);
  return <LottieView ref={ref} source={require('./phone.json')} loop={false} style={{ width: 26, height: 26 }} colorFilters={[{ keypath: '*', color }]} />;
}

const TABS = [
  { key: 'calls', icon: (active, color) => <RingingPhone active={active} color={color} /> },
  // ...
];

2. Progress-driven — drive a smooth Reanimated animation off progress (0→1 as the pill arrives), e.g. a heart that scales up when selected:

import Animated, { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
import { Ionicons } from '@expo/vector-icons';

function PulseHeart({ active, color, progress }: { active: boolean; color: string; progress: SharedValue<number> }) {
  const style = useAnimatedStyle(() => ({ transform: [{ scale: 1 + progress.value * 0.25 }] }));
  return <Animated.View style={style}><Ionicons name={active ? 'heart' : 'heart-outline'} size={24} color={color} /></Animated.View>;
}

const TABS = [
  { key: 'liked', icon: (active, color, progress) => <PulseHeart active={active} color={color} progress={progress} /> },
  // ...
];

Plain icons that ignore the extra args keep working — animation is fully opt-in.

NativeLiquidGlassTabBar (real Apple bar) — in an Expo Router app/(tabs)/_layout.tsx

import { NativeLiquidGlassTabBar } from 'expo-liquid-glass-tabs/router';

export default function TabsLayout() {
  return (
    <NativeLiquidGlassTabBar
      minimizeBehavior="onScrollDown"
      tabs={[
        { name: 'index',   title: 'Home',    sf: 'house.fill',         md: 'home' },
        { name: 'search',  title: 'Search',  sf: 'magnifyingglass',    md: 'search' },
        { name: 'profile', title: 'Profile', sf: 'person.crop.circle', md: 'person' },
      ]}
    />
  );
}

LiquidGlassTabs (auto-switch per platform) — in an Expo Router _layout.tsx

import { Ionicons } from '@expo/vector-icons';
import { LiquidGlassTabs } from 'expo-liquid-glass-tabs/router';

export default function Layout() {
  return (
    <LiquidGlassTabs
      accentColor="#0A84FF"
      nativeTabs={[
        { name: 'index',  title: 'Home',   sf: 'house.fill',      md: 'home' },
        { name: 'search', title: 'Search', sf: 'magnifyingglass', md: 'search' },
      ]}
      customTabs={[
        { key: 'index',  icon: (on, c) => <Ionicons name={on ? 'home' : 'home-outline'} size={24} color={c} /> },
        { key: 'search', icon: (on, c) => <Ionicons name={on ? 'search' : 'search-outline'} size={24} color={c} /> },
      ]}
    />
  );
}

Props

LiquidGlassTabBar

| Prop | Type | Default | Description | |---|---|---|---| | tabs | { key: string; icon: (active, color, progress) => ReactNode }[] | — | Tab list. icon is a render-prop — use any icon. progress (0→1 shared value) lets you animate the icon on select (see Animated icons). | | activeKey | string | — | Key of the active tab. | | onChange | (key: string) => void | — | Called when a tab is selected. | | scrollY | SharedValue<number> | — | Optional. Drives scroll-to-minimize. | | accentColor | string | #0A84FF | Active icon + pill color. | | inactiveColor | string | auto (light/dark) | Inactive icon color. | | tintColor | string | — | Glass tint. | | colorScheme | 'light' \| 'dark' \| 'system' | system | Forces glass/colors light or dark. | | bottomInset | number | 0 | Safe-area bottom padding. | | enableGestures | boolean | true | Press-hold-drag gesture on the pill. |

NativeLiquidGlassTabBar

| Prop | Type | Default | Description | |---|---|---|---| | tabs | { name; title; sf; md }[] | — | name matches the route file; sf/md are SF Symbol / Material icon names. | | minimizeBehavior | 'automatic' \| 'never' \| 'onScrollDown' \| 'onScrollUp' | onScrollDown | Native scroll-minimize. |

LiquidGlassTabs

| Prop | Type | Description | |---|---|---| | nativeTabs | { name; title; sf; md }[] | iOS tab config. | | customTabs | { key; icon }[] | Android tab config (keys match route names). | | accentColor | string | Android pill/accent color. | | minimizeBehavior | see above | iOS scroll-minimize. |

Requirements & notes

  • The genuine Liquid Glass material only renders on an iOS 26 simulator/device built with Xcode 26. It does not appear in Expo Go. On older iOS / Android the custom bar falls back to a blur-based glass; the native bar falls back to the standard system tab bar.
  • NativeLiquidGlassTabBar / LiquidGlassTabs require Expo Router (SDK 54+). Android caps native tabs at 5.

License

MIT © Omar Shayya