React Native Liquid GitHub

Liquid Glass for React Native

Tab bars, headers, toggles, sliders, menus and search that plug straight into React Navigation. Native glass on iOS 26, Compose glass on Android, and every animation runs natively.

iOS 26 UIGlassEffect Android · Kyant0 backdrop New Architecture

A CSS sketch of LiquidBottomTabBar. Click a tab. The real component draws native glass with a liquid drop that follows your finger.

Overview

@mohamed_abd_elnaby/react-native-liquid gives React Native apps the iOS 26 Liquid Glass look on both platforms, with components shaped to drop into React Navigation as custom tab bars and headers.

Installation

yarn add @mohamed_abd_elnaby/react-native-liquid react-native-safe-area-context

Install the navigators you use, for example @react-navigation/bottom-tabs, @react-navigation/material-top-tabs or @react-navigation/native-stack. They are optional peer dependencies.

Then rebuild the native app:

cd ios && pod install && cd ..
npx react-native run-ios
npx react-native run-android

Requirements

  • React Native 0.80+ with the New Architecture (Fabric). Works in React Native CLI apps and Expo dev builds, not in Expo Go.
  • iOS: build with Xcode 26+ to get Liquid Glass on iOS 26 devices. Older iOS versions get a system blur material.
  • Android: Kotlin 2.2+ in the app. Glass needs Android 12+, lens refraction Android 13+. Older versions get a frosted material.

How it works

iOS

  • Glass is UIGlassEffect and UIGlassContainerEffect in Fabric components.
  • The tab bar drop is a clear glass lens driven by a CADisplayLink spring.
  • UISwitch, UISlider and UIButton menus give the system glass controls.

Android

  • Each glass view hosts a small Compose view drawn with AndroidLiquidGlass (vibrancy, blur, lens).
  • The views behind the glass are recorded into a RenderNode every frame, so scrolling content stays live under the bars.
  • Springs run on Choreographer, never on the JS thread.
Content in a separate surface (SurfaceView, some video and map views) is not sampled on Android, and glass shapes do not merge there: LiquidGlassContainer is a plain view on Android.

Bottom tab bar

A floating glass bar for @react-navigation/bottom-tabs. Pressing lifts a glass drop out of the pill. The drop follows your finger, stretches with its speed, magnifies the tabs and melts back into the pill. In light mode the bar is frosted white with a soft gray pill; in dark mode it is frosted dark gray with a darker pill.

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createLiquidBottomTabBar } from '@mohamed_abd_elnaby/react-native-liquid';

const Tab = createBottomTabNavigator();
// Create the tabBar function once, outside of render.
const tabBar = createLiquidBottomTabBar({ search: { placeholder: 'Search' } });

export function Tabs() {
  return (
    <Tab.Navigator tabBar={tabBar}>
      <Tab.Screen name="Home" component={Home}
        options={{ tabBarIcon: ({ color }) => <HomeIcon color={color} /> }} />
      <Tab.Screen name="Settings" component={Settings} />
    </Tab.Navigator>
  );
}
Tapping and dragging across the bottom tab bar: a glass drop lifts out of the pill, follows the finger and melts back
Tap and drag: the drop lifts, follows the finger and melts into the pill.
The bottom tab bar in light and dark mode
Light and dark: frosted white or dark gray, with a soft pill.
The search button morphs into a field and the bar rises above the keyboard
search: the bar rises above the keyboard.

The bar floats over the screens. Pad scrollable content with useBottomTabBarHeight(); the bar reports its height to it.

It reads tabBarIcon, tabBarLabel, tabBarShowLabel, tabBarBadge, tabBarBadgeStyle, tabBarActiveTintColor, tabBarInactiveTintColor, tabBarLabelStyle, tabBarAccessibilityLabel, tabBarButtonTestID, tabBarHideOnKeyboard and tabBarStyle: { display: 'none' }, and emits tabPress and tabLongPress.

OptionDefaultDescription
searchAdds a search button after the bar that morphs into a field with a close button. The tabs fade out and the bar rises above the keyboard. Takes placeholder, value, onChangeText, onSubmit, expanded, defaultExpanded, onExpandedChange. Replaces accessory.
accessoryAny element in a separate glass circle after the bar. Wrap it in a Pressable.
glassStyle'clear''regular' or 'clear'.
glassTintColortheme frostTints the glass. Defaults to colors.tabBarGlass.
blur0.8Frosts the content under the bar, from 0 (clear) to 1.
selectionColortheme pillColor of the resting pill behind the focused tab.
activeTintColor / inactiveTintColorOverride the screen options.
lenstrueLifts the liquid drop while pressing and dragging.
colorScheme'system''system', 'light' or 'dark'.
height62Height of the capsule.
horizontalMargin20Distance from the screen edges.
bottomOffsetsafe areaDistance from the bottom edge.
styleStyle of the capsule.

Top tab bar

A floating segmented bar for @react-navigation/material-top-tabs, with the same drop and the same frosted look as the bottom bar. While swiping pages, the pill and the label colors follow the pager position natively.

Swiping pages moves the pill; dragging the bar lifts the drop
Swipe pages or drag the bar.
The top tab bar in light and dark mode
Same frost and pill as the bottom bar.
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import {
  createLiquidTopTabBar,
  LIQUID_TOP_TAB_BAR_HEIGHT,
} from '@mohamed_abd_elnaby/react-native-liquid';

const TopTab = createMaterialTopTabNavigator();
const tabBar = createLiquidTopTabBar();

<TopTab.Navigator tabBar={tabBar}>
  <TopTab.Screen name="Photos" component={Photos} />
  <TopTab.Screen name="Albums" component={Albums} />
</TopTab.Navigator>

// The bar floats over the pages: pad their content.
<FlatList contentContainerStyle={{ paddingTop: LIQUID_TOP_TAB_BAR_HEIGHT }} … />
OptionDefaultDescription
floatingtrueFloats the bar over the pages so content scrolls under the glass. Pad content with LIQUID_TOP_TAB_BAR_HEIGHT (60).
glassStyle'clear''regular' or 'clear'.
glassTintColortheme frostSame frost as the bottom tab bar by default.
blur0.8Frosts the content under the bar, from 0 to 1.
selectionColortheme pillColor of the pill behind the focused tab.
lensTintColorthemeTints the glass of the drop.
activeTintColor / inactiveTintColorOverride the screen options. The drop uses the active color too.
lenstrueLifts the drop while pressing.
colorScheme'system'Forces light or dark.
height44Height of the capsule.
style / containerStyleStyle of the capsule / of the view around it.

Morphing header

One floating header for the whole app. Wrap the root navigator in LiquidHeaderProvider, turn off the navigators' own headers, and describe each screen's header with useLiquidNavigationHeader. Because the header is shared, it morphs on every push, pop and tab switch: Edit becomes Sort or the back button, two circles fuse into one, and the title blurs across.

Header buttons morphing from Home to Contacts
Home → Contacts: Edit becomes Sort, the two circles merge into one
<LiquidHeaderProvider>
  <Stack.Navigator screenOptions={{ headerShown: false }}>…</Stack.Navigator>
</LiquidHeaderProvider>

// In a screen:
useLiquidNavigationHeader(navigation, {
  title: 'Chats',
  left: { title: editing ? 'Done' : 'Edit', onPress: toggle },
  right: [
    { key: 'story', icon: <PlusIcon />, onPress: newStory },
    { key: 'compose', icon: <ComposeIcon />, tintColor: '#0A84FF' },
  ],
});
  • Items are matched by place. When an item's key changes, its glass resizes and the content blurs across. Same key: nothing moves.
  • Fewer items: the extra ones slide into their neighbour, fuse and shrink. Use null to keep a place empty: right: [closeButton, null] merges the second button into the first, pressed one.
  • subtitle shows the title and subtitle in a glass capsule, like a chat header. stretch: true makes an item take the free width.
  • The back button appears in stacks, not in tabs; backButton overrides it. Pad content with useLiquidHeaderHeight().
Two header buttons merging into the pressed one
+ merges into the compose button

Search

  • Tab bar search screen: createLiquidBottomTabBar({ search: { route: 'Search' } }) and a Search tab screen. The circle morphs into a field, the Search screen opens and reads route.params?.query; closing returns to the previous tab.
  • Search under the header (a contacts list): LiquidSearchHeader with headerHeight={LIQUID_HEADER_HEIGHT}; clear the shared header while searching so the field rises into its place.
  • Header search circle (a settings screen): the search option of useLiquidNavigationHeader.
Search screen opened from the tab bar
Search screen with the tab bar field open

Profile header

LiquidProfileHeader follows an Animated.ScrollView: a full-width photo, then a round avatar over the blurred photo, then a frosted glass bar that fades into the content. useLiquidProfileSnapOffsets() makes the scroll rest on each step, useLiquidProfileEntrance() grows the photo out of the chat header's avatar, and LiquidStickyView pins a control like Media / Links under the bar.

Profile header in its three states
Photo · avatar · bar

Right to left

Every component follows I18nManager.isRTL on iOS and Android: the tab bars fill from the right, search fields and headers mirror, merges slide the other way, and switches and sliders flip. Call I18nManager.forceRTL(true) and restart the app.

AI agents

The package ships CONTEXT.md, written for AI coding agents: the app shell, header recipes, how items morph and merge, which search to use, the profile screen, and the pitfalls. Point your agent at node_modules/@mohamed_abd_elnaby/react-native-liquid/CONTEXT.md.

Glass view & container

import { LiquidGlassView, LiquidGlassContainer } from '@mohamed_abd_elnaby/react-native-liquid';

<LiquidGlassView cornerRadius={24} style={{ padding: 16 }}>
  <Text>On glass</Text>
</LiquidGlassView>

// Neighbouring glass merges and morphs (iOS 26).
<LiquidGlassContainer spacing={8} style={{ flexDirection: 'row', gap: 16 }}>
  <LiquidGlassView morph interactive cornerRadius={22} style={{ width: wide ? 104 : 44, height: 44 }} />
  <LiquidGlassView morph interactive cornerRadius={22} style={{ width: 44, height: 44 }} />
</LiquidGlassContainer>
Two glass buttons morph into a toolbar and its menu opens
Morphing glass in a container, and a bare LiquidMenu inside it.
Keep a container's spacing below the gap between its children. The shapes then fuse only while they morph and rest apart. A larger spacing makes them bulge toward each other.
PropDefaultDescription
glassStyle'regular''clear' is more transparent, for media.
tintColorTints the glass.
interactivefalseNative interactive glass on iOS 26; a press highlight on Android.
cornerRadiusstyle.borderRadiusAbove half the shortest side gives a capsule.
morphfalseAnimates size and position changes as a liquid morph.
colorScheme'system'Forces the light or dark material.
blurRadius8 (clear: 2)Android: backdrop blur in dp.
refractionHeight / refractionAmount12 / 24Android 13+: lens refraction in dp.
chromaticAberration, depthEffectfalseAndroid 13+: lens options.
highlight, shadowtrueAndroid: rim highlight and drop shadow.
spacing (container)0Distance at which glass children start blending (iOS 26).

Buttons

<LiquidButton title="Continue" variant="prominent" onPress={next} />
<LiquidIconButton accessibilityLabel="Add" onPress={add}><PlusIcon /></LiquidIconButton>

LiquidButton: title, icon, variant ('glass' or 'prominent'), tintColor, size ('small' | 'regular' | 'large'), textStyle, plus the glass and press props. LiquidIconButton: children and size (diameter, 44).

Segmented control, toggle, slider and expandable search in use
Segmented control, toggle, slider and expandable search.

Toggle

<LiquidToggle value={enabled} onValueChange={setEnabled} />

Pressing turns the thumb into a clear glass lens that grows past the track, slides while the track fills with color, then settles back into a solid thumb. Native on both platforms (UIKit and Compose), and every part can be recolored. Props: value, onValueChange, disabled, onTintColor (track when on), offTintColor (track when off), thumbTintColor (thumb at rest), lensTintColor (glass lens while pressed), colorScheme, style, accessibilityLabel, testID. Horizontal drags are not stolen by a parent scroll view.

Slider

<LiquidSlider value={volume} onValueChange={setVolume} step={0.1} />

Props: value, minimumValue (0), maximumValue (1), step (0 = continuous), onValueChange, onSlidingStart, onSlidingComplete, disabled, tintColor, colorScheme, style.

Segmented control

<LiquidSegmentedControl segments={['Day', 'Week', 'Month']} selectedIndex={i} onChange={setI} />

Props: segments, selectedIndex, onChange, height (36), glassStyle, colorScheme, activeTintColor, inactiveTintColor, selectionColor, disabled, style, textStyle.

Theme & utilities

const { scheme, colors } = useLiquidColors('system');
if (isLiquidGlassSupported) { … }
  • useLiquidColors(setting?) returns the scheme and the palette used by the components: label, secondaryLabel, tint, success, selection, tabSelection, tabBarGlass, topTabLens, track, thumb.
  • getLiquidColors(scheme) and useLiquidColorScheme(setting) for use outside components.
  • isLiquidGlassSupported: true on iOS 26+ and Android 12+.
  • createLiquidBottomTabBar, createLiquidTopTabBar, createLiquidHeader return render functions; create them once outside of render.

Troubleshooting

"No member named … in …Props" when building iOS

The native props changed. Run pod install again to regenerate codegen, then rebuild.

"Unable to load script" on an Android phone

The phone lost its connection to Metro, often after a USB reconnect. Forward the Metro port again:

adb reverse tcp:8081 tcp:8081

Content does not show through the glass on Android

Views drawn in their own surface (SurfaceView, some video and map views) cannot be sampled. Place glass over regular views.

Glass is blank after navigating back on Android

Update to the latest version: glass views keep their composition while react-native-screens detaches them.