---
title: "Shader Reveal Transition"
description: "Eight reveal shapes — from a noise gate to a barcode shutter — in one compiled shader."
canonical_url: "https://nuxt-motion-block.pages.dev/components/transitions/shader-reveal-transition"
---
# 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

<code-preview label="Preview">
<div className="flex,justify-center,w-full,h-full">
<component-example name="shader-reveal-transition-basic">



</component-example>
</div>

<template v-slot:code="">

```vue
<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>
```

</template>
</code-preview>

## 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.

<code-preview label="Preview">
<div className="w-full">
<component-example name="shader-reveal-transition-variants">



</component-example>
</div>

<template v-slot:code="">

```vue
<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>
```

</template>
</code-preview>

## Customize

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

<code-preview label="Preview">
<div className="w-full">
<component-example name="shader-reveal-transition-custom">



</component-example>
</div>

<template v-slot:code="">
<playground-code name="ShaderRevealTransition">



</playground-code>
</template>
</code-preview>

## Props

<component-props>
<component-prop name="variant" type="'circle' | 'luma' | 'noise' | 'planetary' | 'push' | 'stripes' | 'wipe' | 'zoom'" default="'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.

</component-prop>

<component-prop name="duration" type="number" default="1080">

Total 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.

</component-prop>

<component-prop name="transitionKey" type="string | number" default="0">

Change 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.

</component-prop>

<component-prop name="class" type="string">

Merged onto the root, which is `relative isolate overflow-hidden` — the
overlay and the canvas clip to it.

</component-prop>
</component-props>

## Events

<component-events>
<component-event name="rest" type="() => void">

Fired 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.

</component-event>
</component-events>

## Slots

<component-slots>
<component-slot-row name="default">

The panel's content. Replaced while the cover is opaque.

</component-slot-row>
</component-slots>

## 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:

<table>
<thead>
  <tr>
    <th>
      Variant
    </th>
    
    <th>
      Shape
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        noise
      </code>
      
       <em>
        (default)
      </em>
    </td>
    
    <td>
      A chunky, organic gate torn open by a noise threshold
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        zoom
      </code>
    </td>
    
    <td>
      A large central lens rather than a sweep
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        circle
      </code>
    </td>
    
    <td>
      A noisy iris expanding from the centre
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wipe
      </code>
    </td>
    
    <td>
      A narrow blade with a displaced, noisy edge
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        luma
      </code>
    </td>
    
    <td>
      Horizontal ribbons pulled vertically by luminance
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        planetary
      </code>
    </td>
    
    <td>
      A spiral vortex of displaced arms
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        stripes
      </code>
    </td>
    
    <td>
      A barcode shutter opening under a diagonal sweep
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        push
      </code>
    </td>
    
    <td>
      Vertical bands pushed and pulled by directional pressure
    </td>
  </tr>
</tbody>
</table>


## Sitemap

See the full [sitemap](https://nuxt-motion-block.pages.dev/sitemap.md) for all pages.
