Expo Liquid Glass Tabs for React Native – expo-liquid-lens

Description:

expo-liquid-lens is an Expo and React Native segmented-control component that moves a GPU-rendered refractive glass pill across a row of tabs.

It combines React Native WebGPU, Skia, Reanimated, and Gesture Handler. Ideal for interactive category filters, view switches, and compact in-screen selectors.

See It In Action

Features

  • Real‑time liquid‑glass refraction shader rendered on the GPU (WGSL via react-native-webgpu).
  • Spring‑driven pill position and width that animate as the selection moves.
  • Tap any tab to select it, or drag the pill freely across the row.
  • Compound component API with <Tabs>, <Tabs.Item>, <Tabs.Label>, and <Tabs.Icon>.
  • Works controlled (index + onChange) or uncontrolled (defaultIndex).
  • Theme active/inactive colors and style the pill itself (color, opacity, radius, height, horizontal padding).
  • Align the tab row to start, center, or end.
  • Skia‑baked text labels and vector‑icon glyphs from any @expo/vector-icons family.

How To Use It

Run the Supplied Expo Project

git clone https://github.com/rit3zh/expo-liquid-lens
cd expo-liquid-lens
bun install
# Build and run the native iOS app.
bun ios
# Or build and run the Android app.
bun android

bun ios and bun android call Expo’s native run commands. They compile the native modules used by WebGPU and Skia. Expo Go does not load this stack. The starter currently pins Expo SDK 56, React Native 0.85.3, React Native WebGPU 0.5.15, Skia 2.6.2, Reanimated 4.3.1, and Gesture Handler 2.31.1.

Move It Into an Existing App

Copy the liquid tab implementation as one dependency group:

src/
├── components/
│   └── liquid-tabs/
├── constants/
│   └── liquid-pill-tab.ts
├── ctx/
├── hooks/
├── interface/
├── styles/
└── utils/

The tab source imports its lens engine, gesture logic, context, interfaces, style definitions, and utility functions from those folders. Copy the related modules together. Keep the @/ TypeScript alias from the starter project or rewrite those imports for your own folder structure.

Your native Expo app also needs compatible versions of:

react-native-webgpu
@shopify/react-native-skia
react-native-gesture-handler
react-native-reanimated
react-native-worklets
@expo/vector-icons

React Native WebGPU requires React Native 0.81 or later and does not support the legacy architecture.

Basic Usage

import { Tabs } from "@/components/liquid-tabs";
import { useState } from "react";
import { Text, View } from "react-native";
const PROFILE_SECTIONS = ["Overview", "Activity", "Saved"] as const;
export function ProfileSectionPicker() {
  const [sectionIndex, setSectionIndex] = useState(0);
  return (
    <View style={{ gap: 20 }}>
      <Tabs
        // Controlled state keeps the tab and screen content aligned.
        index={sectionIndex}
        onChange={setSectionIndex}
        activeColor="#FFFFFF"
        inactiveColor="#A7A7A7"
        pill={{
          color: "#FFFFFF",
          opacity: 0.08,
          height: 35,
          paddingX: 14,
        }}
      >
        {PROFILE_SECTIONS.map((section) => (
          <Tabs.Item key={section}>
            <Tabs.Label>{section}</Tabs.Label>
          </Tabs.Item>
        ))}
      </Tabs>
      <Text style={{ color: "#FFFFFF" }}>
        {PROFILE_SECTIONS[sectionIndex]} content
      </Text>
    </View>
  );
}

Use index with onChange when the selected item changes content outside the tab control. Use defaultIndex for local uncontrolled selection.

Add Icons to a Browse Control

import Ionicons from "@expo/vector-icons/Ionicons";
import { Tabs } from "@/components/liquid-tabs";
import { useState } from "react";
export function BrowseModeTabs() {
  const [modeIndex, setModeIndex] = useState(0);
  return (
    <Tabs
      index={modeIndex}
      onChange={setModeIndex}
      activeColor="#F8FAFC"
      inactiveColor="#94A3B8"
      pill={{ color: "#334155", opacity: 0.7, radius: 18 }}
    >
      <Tabs.Item>
        <Tabs.Icon family={Ionicons} name="grid-outline" size={18} />
        <Tabs.Label>Browse</Tabs.Label>
      </Tabs.Item>
      <Tabs.Item>
        <Tabs.Icon family={Ionicons} name="play-circle-outline" size={18} />
        <Tabs.Label>Watchlist</Tabs.Label>
      </Tabs.Item>
    </Tabs>
  );
}

Tune the Lens for a Dense Filter Row

<Tabs
  defaultIndex={1}
  align="start"
  activeColor="#18181B"
  inactiveColor="#71717A"
  pill={{
    color: "#FFFFFF",
    opacity: 0.2,
    radius: 20,
    height: 40,
    paddingX: 18,
  }}
>
  <Tabs.Item>
    <Tabs.Label>All Items</Tabs.Label>
  </Tabs.Item>
  <Tabs.Item>
    <Tabs.Label>In Progress</Tabs.Label>
  </Tabs.Item>
  <Tabs.Item>
    <Tabs.Label>Completed</Tabs.Label>
  </Tabs.Item>
</Tabs>

Use align="start" when the selector belongs to a left-aligned screen layout. A larger height, radius, and paddingX create a more prominent pill.

Tabs API Reference

PropTypeDefaultPurpose
indexnumberNoneControlled active tab index.
defaultIndexnumber0Initial index for uncontrolled state.
onChange(index: number) => voidNoneReceives a new index after tap or drag selection.
activeColorstring#ffd740Selected label and icon color.
inactiveColorstring#ffffffUnselected label and icon color.
align"start" | "center" | "end""center"Horizontal placement of the tab row.
pillobjectSee belowControls the liquid-glass lens appearance.
styleStyleProp<ViewStyle>NoneAdds styles to the outer wrapper.

The pill object accepts color, opacity, radius, height, and paddingX. Its defaults are #ffffff, 0.06, 0, 35, and 14. ([GitHub][1])

Child Components

ComponentPropsPurpose
<Tabs.Item>inactiveColor?Defines one selectable tab.
<Tabs.Label>children: stringSupplies the text rasterized into the tab texture.
<Tabs.Icon>family, name, size?Adds an icon from an Expo vector-icon font family.
useTabs()NoneReturns the current index and select function from tab context.

Alternatives and Related Resources

FAQs

Q: Why does expo-liquid-lens fail in Expo Go?
A: The control depends on native WebGPU and Skia code. Run it through an iOS or Android development build.

Q: Does expo-liquid-lens replace an Expo Router tab bar?
A: No. It works as a horizontal in-screen selector for content categories, filters, views, or local display modes.

Q: Can I use SVG icons in <Tabs.Icon>?
A: No. The component expects a glyph-based font family from @expo/vector-icons. Use an icon font and pass its family, glyph name, and optional size.

Add Comment