Liquid Metal
Animated liquid-metal surface shader with configurability
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated @shopify/react-native-skiaCopy and paste the following code into your project.
component/organisms/liquid-metal
// @ts-checkimport React, { useMemo, memo } from "react";import { View, StyleSheet } from "react-native";import { Canvas, Fill, Skia, Shader, type Uniforms,} from "@shopify/react-native-skia";import { useSharedValue, useFrameCallback, useDerivedValue,} from "react-native-reanimated";import { SHADER_SOURCE, DEFAULTS } from "./conf";import { colorToRGBA } from "./helper";import type { ILiquidMetal, RGBA } from "./types";const shader = Skia.RuntimeEffect.Make(SHADER_SOURCE);export const LiquidMetal: React.FC<ILiquidMetal> & React.FunctionComponent<ILiquidMetal> = memo( ({ width = DEFAULTS.WIDTH, height = DEFAULTS.HEIGHT, borderRadius = DEFAULTS.BORDER_RADIUS, highlightColor = DEFAULTS.HIGHLIGHT, shadowColor = DEFAULTS.SHADOW, density = DEFAULTS.DENSITY, rate = DEFAULTS.RATE, split = DEFAULTS.SPLIT, turbulence = DEFAULTS.TURBULENCE, crispness = DEFAULTS.CRISPNESS, tilt = DEFAULTS.TILT, pulsate = DEFAULTS.PULSATE, halo = DEFAULTS.HALO, asChild = false, children, style, }: ILiquidMetal) => { const tick = useSharedValue<number>(0); useFrameCallback(() => { tick.value += 0.016 * rate; }); const light = useMemo<RGBA>( () => colorToRGBA(highlightColor), [highlightColor], ); const dark = useMemo<RGBA>(() => colorToRGBA(shadowColor), [shadowColor]); const uniforms = useDerivedValue<Uniforms>(() => ({ uDimensions: [width, height], uTick: tick.value, uLight: light, uDark: dark, uDensity: density, uRate: rate, uSplit: split, uTurbulence: turbulence, uCrispness: crispness, uTilt: tilt, uPulsate: pulsate, uHalo: halo, })); if (!shader) return null; const shaderContent: React.ReactNode & React.ReactElement = ( <Canvas style={[StyleSheet.absoluteFill, { borderRadius }]}> <Fill> <Shader source={shader} uniforms={uniforms} /> </Fill> </Canvas> ); if (asChild) { return ( <View style={[styles.wrapper, { width, height, borderRadius }, style]}> {shaderContent} <View style={[styles.content, { borderRadius }]}>{children}</View> </View> ); } return ( <View style={[styles.wrapper, { width, height, borderRadius }, style]}> {shaderContent} </View> ); },);const styles = StyleSheet.create({ wrapper: { position: "relative", overflow: "hidden", backgroundColor: "transparent", }, content: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center", overflow: "hidden", },});export type { ILiquidMetal, RGBA } from "./types";export default memo< React.FC<ILiquidMetal> & React.FunctionComponent<ILiquidMetal>>(LiquidMetal);Usage
import { View, Text, StyleSheet } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import LiquidMetal from "@/components/organisms/liquid-metal";import { SymbolView } from "expo-symbols";export default function App(): React.JSX.Element { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), Coolvetica: require("@/assets/fonts/Coolvetica-Rg.otf"), }); const INPUTS: string[] = [ "Reacticx is awesome!", "UI components made easy.", "Build stunning apps fast.", "Customizable and flexible.", "Join the Reacticx community!", ]; return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={{ justifyContent: "center", alignItems: "center", marginTop: 100, }} > <LiquidMetal asChild width={300} height={60} borderRadius={100}> <View style={{ justifyContent: "center", alignItems: "center", width: 290, height: 50, backgroundColor: "#000", borderRadius: 100, flexDirection: "row", gap: 10, }} > <SymbolView name="oval.portrait.bottomhalf.filled" tintColor={"#fff"} size={18} /> <Text style={{ fontFamily: fontLoaded ? "SfProRounded" : undefined, fontSize: 18, color: "#fff", }} > Reacticx's Liquid Metal </Text> </View> </LiquidMetal> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#000", paddingHorizontal: 24, }, card: { backgroundColor: "rgba(255,255,255,0.08)", borderRadius: 20, padding: 20, top: 120, }, header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", }, title: { color: "#fff", fontSize: 17, fontWeight: "600", }, subtitle: { color: "rgba(255,255,255,0.6)", fontSize: 13, marginTop: 2, }, body: { marginTop: 12, color: "rgba(255,255,255,0.75)", fontSize: 14, lineHeight: 20, }, trigger: { width: 36, height: 36, borderRadius: 10, backgroundColor: "rgba(255,255,255,0.12)", justifyContent: "center", alignItems: "center", }, menu: { backgroundColor: "#fff", }, itemText: { fontSize: 15, color: "#111", }, destructive: { color: "#dc2626", },});Props
React Native Reanimated
React Native Skia
