react-native-liquid-fill
v0.1.0
Published
Liquid progress for React Native: water that fills any shape, with travelling waves, rising bubbles and a surface that sloshes when the phone tilts. Plus a ProgressRing with a second lap past 100%. Reanimated + react-native-svg, UI thread.
Maintainers
Readme
react-native-liquid-fill
Liquid progress for React Native. Water fills any shape, with travelling waves and rising bubbles, and the surface sloshes when the phone tilts. Also includes a ProgressRing that goes round a second time past 100%.
- The water stays level while the phone turns. Tilt it and the surface swings past level and rocks back, the way water does in a glass. Shake it and the waves whip up. Turn it upside down and the water collects at the top of the screen.
- It stays calm when it should. Gravity is separated from the hand moving the phone, a phone lying flat on a table doesn't drag the water to one side, and a dropped frame or a slow sensor never kicks the surface.
- It works in any shape. Use
circle,roundedorrect, or pass any SVGpath(a drop, a bottle, a battery), with an optional outline. - The level springs to a new value instead of jumping.
ProgressRinguses the same spring, so a ring and the water inside it rise together. - Everything runs on the UI thread. Waves, bubbles and the slosh physics all run in Reanimated worklets.
pausedstops them for a screen nobody is looking at. - Screen readers get a progress bar. Both components are progress bars that read out their percentage.
Install
npm install react-native-liquid-fillPeer dependencies:
npx expo install react-native-reanimated react-native-svgFor tilt, also install expo-sensors. It's optional, and without it the water just rolls:
npx expo install expo-sensorsOn Android 12+ the accelerometer only runs at 5 Hz unless your app declares HIGH_SAMPLING_RATE_SENSORS. Add it to app.json for smooth sloshing:
{ "expo": { "android": { "permissions": ["android.permission.HIGH_SAMPLING_RATE_SENSORS"] } } }Quick start
import { LiquidFill } from 'react-native-liquid-fill';
import { useAccelerometerTilt } from 'react-native-liquid-fill/expo';
function Water({ progress }: { progress: number }) {
const tilt = useAccelerometerTilt();
return <LiquidFill size={160} progress={progress} tilt={tilt} accessibilityLabel="Water today" />;
}Leave out tilt and the water stays level and just rolls. That way you don't need expo-sensors at all.
Water in a ring
import { innerSize, LiquidFill, ProgressRing } from 'react-native-liquid-fill';
<ProgressRing
size={220}
strokeWidth={14}
progress={progress}
fill={<LiquidFill size={innerSize(220, 14)} progress={progress} tilt={tilt} />}
>
<Text>{Math.round(progress * 100)}%</Text>
</ProgressRing>progress can go past 1. The ring draws the overflow as a second, lighter lap, while the water just stays full.
Shapes
<LiquidFill size={96} progress={p} /> // circle
<LiquidFill width={150} height={96} shape="rounded" cornerRadius={24} progress={p} />
<LiquidFill width={64} height={120} shape="rounded" outlineColor="#1C2533" outlineWidth={4} progress={p} />
// Any SVG path, drawn in its own box and scaled to width × height:
<LiquidFill
width={100}
height={125}
path="M50 0 C50 0 95 52 95 80 A45 45 0 0 1 5 80 C5 52 50 0 50 0 Z"
pathSize={{ width: 100, height: 125 }}
progress={p}
/>In a circle, the tilted water is exact. In other shapes the surface still stays level and swings, but the water isn't re-measured for the new angle, so treat it as a convincing look rather than a simulation. progress is the height of the water, not its volume.
Pausing
Stop the waves and the sensor when the screen isn't visible:
const focused = useIsFocused(); // react-navigation / expo-router
const tilt = useAccelerometerTilt({ enabled: focused });
<LiquidFill progress={p} tilt={tilt} paused={!focused} />Another sensor library
useTilt(source) takes any accelerometer. source starts delivering readings (in g, the way expo-sensors reports them) and returns a function that stops:
import { useTilt, type TiltSource } from 'react-native-liquid-fill';
const source: TiltSource = (onReading) => {
const sub = mySensor.subscribe(({ x, y, z, timestamp }) => onReading({ x, y, z, timestamp }));
return () => sub.unsubscribe();
};
const tilt = useTilt(source, { invert: false });Keep source stable (define it outside the component). If your library's axes point the other way, pass invert: true.
API
<LiquidFill>
| Prop | Default | |
|---|---|---|
| progress | required | 0–1, the height of the water. |
| size | 120 | Width and height of a square container. |
| width, height | size | For a container that isn't square. |
| shape | 'circle' | 'circle', 'rounded' or 'rect'. A circle becomes an ellipse when width ≠ height. |
| cornerRadius | a fifth of the shorter side | For 'rounded'. |
| path, pathSize | | Any SVG path and the box it's drawn in. Overrides shape. |
| color | #76A1F4 | The water, deep down. |
| surfaceColor | #A8C4F8 | The water at the surface, and the back wave. |
| emptyColor | transparent | The empty part of the container. |
| outlineColor, outlineWidth | none, 0 | A line round the container. |
| bubbles | 7 | 0–7. |
| bubbleColor | #FFFFFF | |
| waveHeight | 1 | 0 for a flat surface. |
| waveSpeed | 1 | |
| tilt | | From useAccelerometerTilt or useTilt. |
| paused | false | Stops waves and bubbles. |
| fillSpring | { damping: 15, stiffness: 80 } | The spring the level moves on. |
| accessibilityLabel, style, testID | | |
<ProgressRing>
| Prop | Default | |
|---|---|---|
| size, progress | required | progress 0–2: past 1 is a second lap. |
| strokeWidth | size / 10 | |
| color, trackColor, overflowColor | #3266AE, #DCE7F8, #76A1F4 | |
| lineCap | 'round' | 'round' or 'butt'. |
| fill | | Drawn inside, e.g. a LiquidFill. |
| children | | Centred on top: the percentage. |
| spring | same as LiquidFill | |
| accessibilityLabel, style, testID | | |
innerSize(size, strokeWidth, gap = 6) returns the diameter a fill can have inside the ring.
Tilt
useAccelerometerTilt({ enabled, invert })fromreact-native-liquid-fill/expo, which uses expo-sensors.useTilt(source, { enabled, invert })fromreact-native-liquid-fill, which works with any sensor.
Both return a LiquidTilt: three shared values (angle, jolt, stir) that LiquidFill reads on the UI thread. Share one tilt between several LiquidFills on a screen.
License
MIT
