Transition
Transition base prototype
Imperative Control
Section titled “Imperative Control”This demo shows imperative state-machine control.
- Enter: Moves into
enteringfrom any valid state (closed → enteringorleaving → entering). Ignored if already entering or entered. - Leave: Moves into
leavingfrom any valid state (entered → leavingorentering → leaving). Ignored if already leaving or closed. - Complete: Advances
enteringtoentered, orleavingtoclosed.
Typical full flow:
Enter → Complete → Leave → CompleteControlled Mode
Section titled “Controlled Mode”This demo drives the state machine through the open prop. Transition uses enterDuration / leaveDuration as a host-neutral completion fallback; a host may also call complete() when its native animation finishes earlier.
- Toggle Open: Flips the
openvalue.- If currently
closed/leaving, settingopen=truetriggersentering → entered. - If currently
entered/entering, settingopen=falsetriggersleaving → closed.
- If currently
- Click Me: Clicking the box itself also toggles
open, behaving the same as the button.
Typical flow:
Toggle Open (show) → Toggle Open (hide)In this demo the CSS transition is set to 0.3s, so entering and leaving will auto-complete after that duration.
Transition is a low-level foundational component for managing the presence lifecycle of elements. It does not render visible content itself; instead, it provides state machine governance, allowing the host platform (CSS, React, Vue, etc.) to drive actual animations based on the state.
Architecture
Section titled “Architecture”asTransition() is a no-argument ordinary once asHook exported by @proto.ui/prototypes-base. Repeated calls in one setup chain return the same projected handle. It governs only the perceptual closed / entering / entered / leaving phases and submits structural intent through run.lifecycle.setPresent() and L1 ViewIntent.
It does not require module ports or host-private facades. Lifecycle callbacks, run.lifecycle.setPresent(), and Core delay() provide the complete public capability boundary needed to implement the policy.
Transition, ViewIntent, and the actual view epoch remain separate facts:
transitionState: closed → entering → entered → leaving → closedViewIntent: detached ↔ presentview epoch: detached ↔ mounting ↔ mounted ↔ unmounting- Enter: ViewIntent becomes present first. The state enters
enteringonly after the new view epoch mounts, preventing partially committed DOM from being revealed. - Leave: The current view is retained throughout
leaving; ViewIntent becomes detached only after completion intoclosed. - Restore: Detaching does not dispose the Proto instance. Controls, state, and prop watchers remain alive and can create a fresh view epoch later.
This makes the state machine work correctly with CSS transitions, because the DOM element is guaranteed to exist during entering and leaving even when the adapter would otherwise remove it immediately.
Relationship with lifecycle ViewIntent
Section titled “Relationship with lifecycle ViewIntent”ViewIntent is Transition’s only structural-intent path. isPresent is perceptual only, and closed does not mean the Proto instance has been disposed.
- RuntimeSession continues syncing props and dispatching watchers while detached, so controlled
open=false → truecan request its own view without a mount-before-update deadlock. - Adapter owners retain the Proto instance and create or release React, Vue, or WC view epochs from the latest ViewIntent.
- The reveal barrier keeps a fresh view hidden until its first commit and effects replay are coherent; React/Vue no longer need a double-RAF baseline.
This separation means:
- Transition users deal only with the state machine. Structural coordination goes through
run.lifecycle.setPresent()rather than adapter-specific APIs. - Adapters deal only with ViewIntent and view epochs. React, Vue, and WC do not need to interpret the business difference between
enteringandentered. - A Proto instance can survive across view epochs. React/Vue may render
null, while WC may release its internal view; neither operation loses controls or prototype state.
Best Practices
Section titled “Best Practices”1. Always pair state changes with CSS transitions
Section titled “1. Always pair state changes with CSS transitions”Transition only manages logical state. If you do not provide CSS for data-transition-state="entering" and data-transition-state="leaving", the element will appear/disappear instantly when complete() is called.
2. Use the duration fallback and optionally provide a host completion signal
Section titled “2. Use the duration fallback and optionally provide a host completion signal”By default Transition completes after enterDuration / leaveDuration. If the host animation finishes earlier, observe that signal and call controls.complete(); the stale fallback task is canceled.
Typical controlled-mode wiring:
// After setting open=true and applying CSS transitionel.addEventListener('transitionend', () => { exposes.controls.complete();});3. Use appear for mount-time animation
Section titled “3. Use appear for mount-time animation”If you want an element to animate in when it first renders, set appear: true. Otherwise the element will jump straight to entered on mount without playing the enter animation.
4. Choose an interrupt strategy that matches your UX
Section titled “4. Choose an interrupt strategy that matches your UX”'reverse'(default): Best for most UI (menus, tooltips). If the user toggles quickly, the animation reverses direction smoothly.'wait': Best when you must guarantee every transition finishes (e.g. a multi-step wizard whose background panel should not skip animations).'immediate': Best for snappy stateless toggles where you do not care about partial animations. The current animation is discarded and the new one starts from a clean state.
5. Do not modify transition state directly
Section titled “5. Do not modify transition state directly”Always use the exposed controls (controls.enter(), controls.leave(), controls.complete()) or the open prop. Mutating transitionState directly bypasses the state-machine and ViewIntent ordering constraints.
6. Keep DOM children stable during leaving
Section titled “6. Keep DOM children stable during leaving”Transition retains the current view during leaving. Avoid destroying child state (such as tearing down a <video> or canceling requests) until closed or the actual view-unmount lifecycle.
State Machine
Section titled “State Machine”closed → entering → entered → leaving → closed- closed: Perceptually absent; it normally requests a detached ViewIntent but does not mean the Proto instance is disposed
- entering: Entering animation in progress, DOM exists, entering styles applied
- entered: Fully visible, stable state
- leaving: Leaving animation in progress, DOM still exists, leaving styles applied
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled mode: target presence state |
defaultOpen | boolean | false | Uncontrolled mode: initial state |
appear | boolean | false | Whether to play enter animation on mount |
enterDuration | number | 300 | Expected enter animation duration (ms) |
leaveDuration | number | 200 | Expected leave animation duration (ms) |
interrupt | 'reverse' | 'wait' | 'immediate' | 'reverse' | Interruption strategy |
Imperative API
Section titled “Imperative API”Accessed via the controls expose:
enter(): Trigger enterleave(): Trigger leavecomplete(): Mark current transition complete, advance to next state
v0 Scope
Section titled “v0 Scope”base-transition v0 only manages the presence lifecycle of a single element (closed → entering → entered → leaving → closed).
Nested exit coordination (e.g., an outer Dialog waiting for an inner child to finish leaving before closing) belongs to a separate transition-group / boundary abstraction and is intentionally out of scope for v0.