All components
0:00/0:00

Recorded on iPhone. Six artworks tide in one after another from the centre of the screen, then loop back to the first.

Shader

Tideline Reveal

The next image washes in like a tide, along a ragged, noise-shaped edge with a warm glow.

Features

  • Each new image washes in like a wave running up a beach
  • Smooth from start to finish, with no flicker
  • Starts wherever you tap, or from a spot you choose
  • A soft glow outlines the wave as it moves
  • Three ready-made styles: default, calm and stormy
  • A gentle vibration you can feel as each image arrives

Built with

  • Skia runtime shader
  • Baked fBm noise
  • Reanimated
  • Pulsar haptics

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 @shopify/react-native-skia react-native-reanimated react-native-worklets react-native-pulsar
2Run a development build (Expo Go can't load Skia or Pulsar)
npx expo run:ios   # or: npx expo run:android

Usage

import { TidelineReveal } from '@/components/tideline-reveal';

const photos = [require('./a.jpg'), require('./b.jpg'), 'https://…/c.jpg'];

export function Gallery() {
  return <TidelineReveal images={photos} />;
}

Tap anywhere on the image to reveal the next one.

Examples

Presets

Change the character of the edge, then override any single value.

<TidelineReveal images={photos} preset="calm" />

<TidelineReveal images={photos} preset="stormy" config={{ duration: 1400 }} />

From the centre, without the glow

Every reveal starts in the middle, with no warm edge and no dark overlay.

<TidelineReveal
  images={photos}
  origin="center"
  glowIntensity={0}
  overlayOpacity={0}
/>

Driven from outside

Your own buttons, with tapping turned off.

function Slideshow() {
  const reveal = useRef<TidelineRevealHandle>(null);

  return (
    <View>
      <TidelineReveal ref={reveal} images={photos} interactive={false} />

      <View style={styles.controls}>
        <Button title="Back" onPress={() => reveal.current?.previous()} />
        <Button title="Next" onPress={() => reveal.current?.next()} />
      </View>
    </View>
  );
}

Props

PropTypeDefaultDescription
imagesDataSourceParam[]Requiredrequire() or URLs. Keep the array stable between renders.
preset"default" | "calm" | "stormy""default"The edge's character: fingered and ragged, soft and round with a slower swell, or torn into tendrils.
origin"touch" | "center" | { x, y }"touch"Where each reveal starts: where the image was tapped, the centre, or a fixed point you give. With "touch", ref calls start from the centre.
glowIntensitynumber0.18Brightness of the glow along the edge, 0–1. 0 removes it.
overlayOpacitynumber0.4Dark overlay on the new image, which fades as it lands. 0 removes it.
loopbooleantrueAfter the last image comes the first.
onIndexChange(index: number) => void—Called as each reveal starts.
childrenReactNode—Views laid over the image. They stay put while it changes.
configPartial<TidelineRevealConfig>—Fine tuning. See Advanced.
styleStyleProp<ViewStyle>—Fills its parent unless you give it a height or flex.

Advanced

Everything most apps never need to touch.

Every field is optional and overrides the preset. Lengths are in points.

FieldDefaultDescription
duration900Milliseconds one reveal takes.
raggedness0.35How far the edge wanders. 0 is a circle.
raggedFloor45How ragged it is right at the start, so it never opens as a circle.
noiseScale40Size of the edge's lobes. Higher is broader.
warp2.5Swirl. 0 gives round blobs, 2.5 fingers, 4 tendrils.
feather10Softness across the edge.
glowWidth26Width of the glow along the edge.
glowColor"#FFFCF0"Glow colour.
fit"cover""cover" crops to fill; "contain" letterboxes.

MethodDescription
next()Reveals the next image.
previous()Reveals the previous image.
revealTo(index)Reveals that image, wrapped with loop or clamped without it.

PropDefaultDescription
initialIndex0Image shown first. Read once; use the ref to move later.
interactivetrueOff, tapping does nothing; the ref and screen readers still move it.
hapticstrueA soft vibration that swells with each reveal.
paintOverlay—(canvas, size, index) => void. Draws with Skia into each image before it is revealed, so text dissolves with the photo. Use fonts loaded through Skia, and keep the function stable: a new one replays the reveal.
onReady—Called once the first image is on screen.
onRevealComplete—Called when a reveal lands, with the index now shown.
accessibilityLabel"Gallery"Read before "2 of 4".
ref—A TidelineRevealHandle. See Ref methods.

  • Taps during a reveal are ignored, so a double tap never cuts one short.
  • Buttons among the children still work alongside tapping the image.
  • Haptics need a physical device.
  • Nothing runs while it sits still: no clock, no redraw.

  • Screen readers read it as "Gallery, 2 of 4", and swiping up or down moves between images.
  • With Reduce Motion on, images simply crossfade: no wave, glow or vibration.

How it works

A Skia shader draws the edge as a line on a noise-warped map of distances from where the reveal starts. During a reveal only that line moves outward, so a pixel can never flip back, which is why the edge never flickers. The noise is baked into a texture once, so each frame costs a single texture read per pixel.

Next componentLiquid Metal Button