# Enter and Exit Transitions
[Full-page experience](/experience/visibility-transitions/) · [Open playground](/playground/#/examples/composition/visibility-transitions)


Compare everyday transitions and energy effects on the same grouped subject. Use the effect controls, Hide and Show to test both directions, and reverse midway. Exit + Unmount and Remount demonstrate the difference between hiding pixels and removing a component.

[Open in the editor](/playground/#/examples/composition/visibility-transitions) · [Energy prototype](/examples/energy-transitions/) · [Effects gallery](/catalogs/)

## How it works

`useVisibilityTransition` returns a mounted controller. Its `binding` attaches to the subject's `style.transition`; `show()` and `hide()` request transitions and return completion results. The reusable effects include `fade`, `slide`, `zoom`, `wipe`, `blurReveal`, `dissolve`, `hologram`, `bifrost`, and `embers`.

## Integration boundaries

This API is documented as experimental. Effects change pixels, not layout or input targeting. A hidden subject does not automatically become noninteractive; applications own that policy. One binding supports one receiver in its Pibbl root. Wrap multiple primitives in a `Group` to transition them together.

The 800 by 560 source is self-contained. Bifrost, Hologram, and Embers lazily compile embedded WASM; a restrictive CSP must allow WebAssembly compilation. No remote asset fetch is required. Pibbl owns playback and disposal. The example starts automatically unless reduced motion is enabled. Applications should also choose their own reduced-motion policy, such as immediate state changes instead of energy effects.

## APIs and agent guidance

- [Transition definitions, presets, completion, and limitations](/reference/hooks/use-visibility-transition/)
- [Animation ownership and scheduling](/agents/topics/animation/)

## Verification

Try each effect in both directions. Reverse a transition before it finishes. Exit and unmount the subject, then remount it. Check completion status rather than assuming a fixed timer means the effect finished; particle tails may continue after the material disappears.

## Related APIs

- [useVisibilityTransition](/reference/hooks/use-visibility-transition/)
- [fade](/reference/hooks/use-visibility-transition/)
- [embers](/reference/hooks/use-visibility-transition/)
- [hologram](/reference/hooks/use-visibility-transition/)
- [bifrost](/reference/hooks/use-visibility-transition/)
- [Group](/reference/components/group/)

## Complete source

Host setup for this source: use a canvas with width 800 and height 560, compile with `jsx: "react-jsx"` and `jsxImportSource: "@pibbl/core"`, import its default `mount`, and call `const controller = mount(canvas)` after attaching the canvas. Call `controller.dispose()` before removing it.

### visibility-transitions.example.tsx

```tsx
import { pibbl, Group, Rectangle, Circle, Text, fade, embers, hologram, bifrost, slide, zoom, wipe, blurReveal, dissolve, useVisibilityTransition, useSignal, type PibblController, type PibblVisibilityTransition } from '@pibbl/core';

const effects = ['bifrost', 'hologram', 'embers', 'fade', 'slide', 'zoom', 'wipe', 'blur', 'dissolve'] as const;
type Effect = typeof effects[number];
const region = { x: 100, y: 145, width: 600, height: 240 };
const definitions = { bifrost: bifrost({ region, entry: { x: 400, y: -240 } }), hologram: hologram({ region }), embers: embers({ region, charDepth: 65, burnWidth: 12, wind: 100, seed: 4, linger: 700 }), fade: fade(), slide: slide({ x: -100, y: 0 }), zoom: zoom({ origin: { x: 400, y: 265 }, from: .15 }), wipe: wipe({ region }), blur: blurReveal(), dissolve: dissolve({ region, cellSize: 6, seed: 4 }) };

function Subject({ effect, register }: { effect: Effect; register: (handle: PibblVisibilityTransition) => void }) {
  const transition = useVisibilityTransition({ initial: true, enter: definitions[effect], duration: 900 });
  register(transition);
  return <Group style={{ transition: transition.binding }}>
    <Rectangle style={{ left: 100, top: 145, width: 600, height: 240, cornerRadius: 20, fill: '#254c64' }} />
    <Circle style={{ cx: 232, cy: 267, radius: 75, fill: '#59e2cc' }} />
    <Circle style={{ cx: 282, cy: 267, radius: 75, fill: '#59e2cc' }} />
    <Text style={{ left: 394, top: 238, font: '600 28px sans-serif', fill: '#f0faf7' }}>Ordinary Pibbl content</Text>
    <Text style={{ left: 394, top: 281, font: '15px sans-serif', fill: '#b8d6df' }}>Text, shapes, and overlapping children.</Text>
    <Text style={{ left: 394, top: 315, font: '15px sans-serif', fill: '#b8d6df' }}>{`One subject. ${effect.toUpperCase()} transition.`}</Text>
  </Group>;
}

function Demo() {
  const effect = useSignal<Effect>('bifrost');
  const mounted = useSignal(true), message = useSignal('Ready. Hide, reverse, or exit and unmount.');
  // A plain callback hands the mounted handle to event closures without scheduling a render.
  let handle: PibblVisibilityTransition | undefined;
  const register = (value: PibblVisibilityTransition) => { handle = value; };
  const run = async (action: 'show' | 'hide' | 'remove') => {
    const current = handle;
    if (!current) { message.set('The subject is unmounted. Choose Remount.'); return; }
    message.set(action === 'show' ? 'Entering…' : effect.get() === 'embers' ? 'Exiting. Waiting for the ash to settle…' : 'Exiting…');
    const result = await (action === 'show' ? current.show() : current.hide());
    message.set(`Request ${result.requestId}: ${result.phase} ${result.status}`);
    if (action === 'remove' && result.status === 'finished' && !current.visible.get()) {
      mounted.set(false); message.set('Exit finished. Subject unmounted safely.');
    }
  };
  const buttons = [
    { label: 'SHOW', action: () => { void run('show'); } },
    { label: 'HIDE', action: () => { void run('hide'); } },
    { label: 'EXIT + UNMOUNT', action: () => { void run('remove'); } },
    { label: 'REMOUNT', action: () => { mounted.set(true); message.set('Mounted with a fresh transition owner.'); } },
  ];
  return <Group>
    <Rectangle style={{ width: 800, height: 560, fill: '#0a1723' }} />
    <Text style={{ left: 40, top: 48, font: '12px monospace', fill: '#88aebc' }}>PUBLIC API / TRANSITION LIBRARY</Text>
    <Text style={{ left: 40, top: 95, font: '600 30px sans-serif', fill: '#ecf8f4' }}>Visibility transitions</Text>
    <Text style={{ left: 400, top: 275, textAlign: 'center', font: '14px sans-serif', fill: '#7e9fad' }}>The subject is hidden or unmounted.</Text>
    <Rectangle style={{ left: 600, top: 84, width: 160, height: 38, cornerRadius: 6, fill: '#20394b', cursor: 'pointer' }} onClick={() => { effect.update(value => effects[(effects.indexOf(value) + 1) % effects.length]!); mounted.set(true); }} />
    <Text pointerEvents="none" style={{ left: 680, top: 103, textAlign: 'center', textBaseline: 'middle', font: '600 12px sans-serif', fill: '#ecf8f4' }}>EFFECT: {effect.get().toUpperCase()}</Text>
    {mounted.get() && <Subject key={effect.get()} effect={effect.get()} register={register} />}
    {buttons.map((button, i) => <Group key={button.label}>
      <Rectangle style={{ left: 40 + i * 184, top: 425, width: 168, height: 42, cornerRadius: 6, fill: '#20394b', cursor: 'pointer' }} onClick={button.action} />
      <Text pointerEvents="none" style={{ left: 124 + i * 184, top: 446, textBaseline: 'middle', textAlign: 'center', font: '600 12px sans-serif', fill: '#ecf8f4' }}>{button.label}</Text>
    </Group>)}
    <Text style={{ left: 40, top: 504, font: '14px monospace', fill: '#76e4ce' }}>{message.get()}</Text>
    <Text style={{ left: 40, top: 540, font: '12px sans-serif', fill: '#88aebc' }}>Click EFFECT to cycle nine presets. Show reverses a pending exit.</Text>
  </Group>;
}
export default function mount(canvas: HTMLCanvasElement): PibblController {
  return pibbl(canvas, <Demo />);
}

```

## Documentation version

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.
