Transitions

Shader Reveal Transition

Eight reveal shapes — from a noise gate to a barcode shutter — in one compiled shader.

A content-swap transition whose overlay is drawn by one fragment shader in eight different shapes: a turbulent noise gate, a zoom lens, an iris, a blade wipe, ribboned luminance, a spiral, a barcode shutter and a vertical push.

All eight share a single compiled program. Each branch writes a mask (how much of this pixel is covered) and a glow (a bright leading edge), and a shared paint function turns those into premultiplied output — which is why a live variant control never forces a shader recompile on the frame it changes.

Like the other shader transitions, Vue's slot updates immediately, so the swap happens behind a cover that opens the sweep fully opaque and clears over the first fifth of it.

Usage

ComposeDraft message
idle
<template>
  <div class="flex w-full max-w-md flex-col gap-4">
    <MShaderRevealTransition
      :transition-key="transitionKey"
      class="overflow-hidden rounded-2xl"
      @rest="rested = true"
    >
      <div
        class="flex h-44 flex-col items-start justify-end gap-1 p-6"
        :class="sent ? 'bg-primary text-white' : 'bg-accented text-dimmed'"
      >
        <span class="text-xs font-medium uppercase tracking-widest opacity-70">
          {{ sent ? 'Sent' : 'Compose' }}
        </span>
        <span class="text-xl font-semibold">
          {{ sent ? 'Message delivered' : 'Draft message' }}
        </span>
      </div>
    </MShaderRevealTransition>

    <div class="flex items-center gap-3">
      <UButton
        :label="sent ? 'New draft' : 'Send message'"
        icon="i-lucide-arrow-left-right"
        @click="swap"
      />
      <span class="text-xs text-muted">{{ status }}</span>
    </div>
  </div>
</template>

<script setup lang="ts">
const sent = ref(false)
const transitionKey = ref(0)
const rested = ref(false)

const status = computed(() => {
  if (transitionKey.value === 0) return 'idle'
  return rested.value ? 'rested' : 'sweeping…'
})

function swap() {
  // The slot updates immediately; the cover that opens the sweep is fully
  // opaque on the first frame, so content and key change on the same tick.
  sent.value = !sent.value
  rested.value = false
  transitionKey.value++
}
</script>

Examples

Reveal shapes

Each of the eight shapes in the branch chain, replayed on selection. variant writes a single uniform, so switching mid-sweep takes effect on the next frame.

noise

Eight reveals, one program

<template>
  <div class="flex w-full max-w-md flex-col gap-4">
    <MShaderRevealTransition
      :transition-key="transitionKey"
      :variant="variant"
      class="h-44 rounded-2xl"
    >
      <div class="grid h-full place-items-center bg-gradient-to-br from-sky-500 to-blue-950 text-center text-white">
        <div class="space-y-1 px-6">
          <p class="text-xs uppercase tracking-widest text-white/60">
            {{ variant }}
          </p>
          <p class="text-2xl font-semibold">
            Eight reveals, one program
          </p>
        </div>
      </div>
    </MShaderRevealTransition>

    <div class="flex flex-wrap gap-2">
      <UButton
        v-for="name in variants"
        :key="name"
        size="sm"
        :label="name"
        color="neutral"
        :variant="variant === name ? 'solid' : 'outline'"
        @click="select(name)"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
const variants = [
  'noise',
  'zoom',
  'circle',
  'luma',
  'planetary',
  'push',
  'stripes',
  'wipe',
] as const

type Variant = typeof variants[number]

const variant = ref<Variant>('noise')
const transitionKey = ref(0)

function select(name: Variant) {
  variant.value = name
  transitionKey.value++
}
</script>

Customize

Adjust the variant, duration and replay live in the playground below.

Shader reveal

Content behind the reveal

Customize

Duration1080
Replay1
VariantNoise

Props

Prop Type Default Description
variant'circle' | 'luma' | 'noise' | 'planetary' | 'push' | 'stripes' | 'wipe' | 'zoom''noise'Which reveal to sweep. Written to a uVariant uniform — one program serves all eight, so switching variant mid-sweep takes effect on the next frame without a recompile.
durationnumber1080Total sweep time in milliseconds, eased with easeOutQuart. Clamped up to 80 — below that a sweep finishes inside a single frame and reads as a jump cut rather than as motion.
transitionKeystring | number0Change it to play the transition. Bump it on the same tick as your content swap; the cover that opens the sweep is what hides the jump.
classstring-Merged onto the root, which is relative isolate overflow-hidden — the overlay and the canvas clip to it.

Events

Event Payload Description
rest() => voidFired once, after the sweep, the closing fade and the content swap have all settled. Under prefers-reduced-motion it fires immediately, because the swap still has to complete even though the sweep does not run.

Slots

Slot Props Description
default-The panel's content. Replaced while the cover is opaque.

Accessibility

The overlay, the cover and the canvas are aria-hidden="true", so the transition contributes nothing to the accessibility tree beyond your own slot content — and a content swap should read the same way to a screen reader as it does on screen. Announce the new state yourself if it is meaningful.

Under prefers-reduced-motion the sweep does not run: the content swaps immediately and rest fires synchronously.

Performance

  • One compiled WebGL1 program for all eight variants, selected by a single float uniform. A variant picker never triggers a recompile.
  • The overlay deliberately stops short of full opacity (it caps at 0.92), so the content underneath stays faintly legible through the densest part of the sweep — pushed to 1.0 it would read as a flat panel wiping across rather than as a reveal passing over live content.
  • The reveal holds at full coverage until three-quarters of the sweep has passed, then fades — without that hold, the last thing on screen would be a dissolving shape instead of the finished reveal.
  • Replaying mid-sweep restarts cleanly: the previous playback is cancelled and its frame callback released before the new one begins.
  • If the browser refuses a WebGL context, a skewed gradient wipes across the panel on the same timing and rest still fires — a noise threshold cannot be expressed in CSS, but the transition still happens, still takes the requested time, and still answers its caller.

Variants

Eight shapes in one branch chain, each with its own colour logic:

VariantShape
noise (default)A chunky, organic gate torn open by a noise threshold
zoomA large central lens rather than a sweep
circleA noisy iris expanding from the centre
wipeA narrow blade with a displaced, noisy edge
lumaHorizontal ribbons pulled vertically by luminance
planetaryA spiral vortex of displaced arms
stripesA barcode shutter opening under a diagonal sweep
pushVertical bands pushed and pulled by directional pressure
Copyright © 2026