All components
0:00/0:00

Recorded on iPhone. The orb pours into the Expo mark, then into a typed "expo". The settings sheet then picks the X logo and tints the particles gold.

Shader

Particle Morph

A glowing orb of 240,000 particles that pours into your logo, a word or an emoji, and back again.

Features

  • 240,000 glowing particles that pour into your logo, a word or an emoji
  • Tap and the orb becomes your shape. Tap again and it gathers back up
  • Drag a finger through it and the particles scatter, then spring back
  • Use your brand colour, over black or your own background
  • Calm, default or lively, depending on the mood you want
  • Works on iPhone and Android from the same code, and looks the same on both
  • Runs on the GPU, so the rest of your app stays smooth

Built with

  • WebGPU
  • WGSL compute shaders
  • react-native-webgpu
  • Skia

Early access

Checkout opens soon

TesseraUI is in early access. Leave your email and we'll let you know the moment you can buy, before anyone else.

No spam. One email when checkout opens.

Or get it with Pro

Installation

1Install dependencies
npx expo install react-native-webgpu @shopify/react-native-skia react-native-reanimated react-native-worklets react-native-gesture-handler
2Wrap your app in GestureHandlerRootView
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Stack />
    </GestureHandlerRootView>
  );
}
3Android only: raise minSdkVersion to 26 (WebGPU needs it)
npx expo install expo-build-properties

Then add the plugin to app.json:

3Android only: raise minSdkVersion to 26 (WebGPU needs it)
{
  "expo": {
    "plugins": [
      ["expo-build-properties", { "android": { "minSdkVersion": 26 } }]
    ]
  }
}
4Run a development build (Expo Go can't load WebGPU)
npx expo run:ios   # or: npx expo run:android

Usage

import { ParticleMorph } from '@/components/particle-morph';

export function Splash() {
  return <ParticleMorph svg={require('./logo.svg')} />;
}

Tap the orb to pour it into your logo, and tap again to bring it back.

Examples

Any line of text or emoji becomes the shape.

<ParticleMorph text="hello" />

Your colour, your background

Tint the grains and let them glow over your own screen.

<ParticleMorph
  svg={logo}
  color="#ff7ab6"
  backgroundColor="transparent"
  preset="calm"
/>

A splash screen

Pour into the logo as soon as it's ready, then move on.

<ParticleMorph
  svg={logo}
  morphed
  interactive={false}
  onMorphEnd={() => router.replace('/home')}
/>

Props

PropTypeDefaultDescription
svgDataSourceParam—The SVG to pour into: require('./logo.svg') or a URL. Filled shapes read best.
textstring""A line of text or emoji. While it has any, it is the shape.
colorstring"#dbe6ff"Grain colour, as #rrggbb.
backgroundColorstring"#000000"What the grains glow over, or "transparent". Dark reads best.
preset"default" | "calm" | "lively""default"The feel: a slow turn and gentle drift, something slower and softer, or fast and restless.
morphedboolean—true shows the shape, false the orb. Leave it out and taps decide.
onMorphEnd(morphed: boolean) => void—Called when a pour finishes, with whether it ended on the shape.
configPartial<ParticleMorphConfig>—Fine tuning. See Advanced.
styleStyleProp<ViewStyle>—Fills its parent by default.

Advanced

Everything most apps never need to touch.

Every field is optional, overrides the preset, and applies on the next frame.

FieldDefaultDescription
brightness1.2Overall glow, about 0.4–2.5.
grainSize1.5Device pixels a grain covers when in focus.
turbulence0.05How far the drift carries a grain. 0 is still.
spinPeriod36Seconds per full turn of the orb.
morphDuration1800Milliseconds the pour takes.
touchRadius0.16Reach of a finger through the grains, where the orb's radius is 1.
touchForce4How hard a finger shoves the grains. 0 turns the push off but keeps taps.
shapeSize0.85The shape's longest side as a share of the orb's width.
fontFamilynullA system font for text, such as "Georgia". Fonts bundled with your app aren't visible here.
fontWeight700Weight for text, 100–900.

MethodDescription
showShape()Pours into the shape, or into the newest one if it has changed.
showOrb()Gathers back into the orb.
toggle()Same as a tap.

PropDefaultDescription
onMorphedChange—Called when the state changes. With morphed set, called with the state a tap asks for.
onTap—Called on every tap.
interactivetrueOff, taps and touch do nothing; only morphed and the ref move it.
pausedfalseStops rendering entirely, for example while the screen is hidden.
count240000Number of grains. Lower it for older devices; changing it rebuilds the GPU buffers.
accessibilityLabel"Particle orb"Read by screen readers.
ref—A ParticleMorphHandle. See Ref methods.

  • A tap mid-pour turns it around from wherever it is.
  • Changing text or svg prepares the next shape a little each frame, so nothing stalls. Left to taps, it waits for the next tap; with morphed on, it pours into the new shape as soon as it's ready.
  • It sleeps on its own while the app is in the background, and carries on without a jump.
  • Works on iOS and Android, not web. Android needs API 26 or later. Without a fontFamily, text uses each platform's own sans.

  • Screen readers announce it as a button when it's interactive, and a double-tap toggles the shape.
  • With Reduce Motion on, the drift, spin and swing slow to a gentle breath.

How it works

Every grain has a fixed home in the orb. Each frame a WebGPU compute pass drifts, spins and lights all 240,000 of them and blends each toward its spot on the shape, then a render pass draws them as soft, additive points. Grains are paired with their targets by angle, which is why the orb visibly pours into the shape instead of scattering. Nothing touches React while it animates.

Next componentProgressive Blur