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-iconsfamily.
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 androidbun 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-iconsReact 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
| Prop | Type | Default | Purpose |
|---|---|---|---|
index | number | None | Controlled active tab index. |
defaultIndex | number | 0 | Initial index for uncontrolled state. |
onChange | (index: number) => void | None | Receives a new index after tap or drag selection. |
activeColor | string | #ffd740 | Selected label and icon color. |
inactiveColor | string | #ffffff | Unselected label and icon color. |
align | "start" | "center" | "end" | "center" | Horizontal placement of the tab row. |
pill | object | See below | Controls the liquid-glass lens appearance. |
style | StyleProp<ViewStyle> | None | Adds 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
| Component | Props | Purpose |
|---|---|---|
<Tabs.Item> | inactiveColor? | Defines one selectable tab. |
<Tabs.Label> | children: string | Supplies the text rasterized into the tab texture. |
<Tabs.Icon> | family, name, size? | Adds an icon from an Expo vector-icon font family. |
useTabs() | None | Returns the current index and select function from tab context. |
Alternatives and Related Resources
- Glassmorphic Pill Tab UI for React Native
- React Native Blur View with Liquid Glass Effect
- React Native Collapsible Header Tabs Component
- Animated Tabbar For React Native
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.





