Liquid Metal

Animated liquid-metal surface shader with configurability

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated @shopify/react-native-skia

Copy 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