Entrance. Opacity and a 12px lift, siblings staggered.
Four motions.
No dials
Cube is a JavaScript animation library with no dials. rise, leave, morph and reveal each ship with the duration, curve and stagger chosen for that job. Web Animations API underneath, zero dependencies.
Exit. Opacity and a 12px drop, held until you remove the node. Half the entrance.
State change. Text keeps shared leading letters and blurs the rest in and out. Icons crossfade as whole faces. The parent’s width follows.
Below the fold. Hidden until it enters the viewport, then rises once. Scroll the box.
API
import { rise, leave, morph, reveal } from "cube-motion";
rise(".hero", { targets: "children" }); // staggered entrance
leave(toast); // staggered exit, holds the end state
morph(oldIcon, newIcon); // one state into the next
reveal(".card"); // rise when scrolled into viewReact
import { Rise, Morph, Reveal } from "cube-motion/react";
<Rise as="section" targets="children" className="hero"> // stagger children
<h1>Four motions.</h1>
<button onClick={save}>
<Morph active={saved} off="Save" on="Saved" />
</button>
</Rise>
<Rise show={open} className="toast">Saved</Rise> // rises in, leaves before unmount
<Reveal as="ul" targets="children" className="cards">{cards}</Reveal><script setup>
import { Rise, Morph, Reveal } from "cube-motion/vue";
</script>
<template>
<Rise as="section" targets="children" class="hero"> <!-- stagger children -->
<h1>Four motions.</h1>
<button @click="save"><Morph :active="saved" off="Save" on="Saved" /></button>
</Rise>
<Rise :show="open" class="toast">Saved</Rise> <!-- rises in, leaves before unmount -->
<Reveal as="ul" targets="children" class="cards">
<li v-for="card in cards" :key="card.id">{{ card.title }}</li>
</Reveal>
</template>import { Rise, Morph, Reveal } from "cube-motion/solid";
<Rise as="section" targets="children" class="hero"> // stagger children
<h1>Four motions.</h1>
<button onClick={save}>
<Morph active={saved()} off="Save" on="Saved" />
</button>
</Rise>
<Rise show={open()} class="toast">Saved</Rise> // rises in, leaves before unmount
<Reveal as="ul" targets="children" class="cards">{cards}</Reveal><script>
import { rise, leave, morph, reveal } from "cube-motion/svelte";
</script>
<button onclick={save}>
<span class="faces" use:morph={saved}>
<span class:inactive={saved} aria-hidden={saved} inert={saved}>Save</span>
<span class:inactive={!saved} aria-hidden={!saved} inert={!saved}>Saved</span>
</span>
</button>
{#if open}<div class="toast" in:rise out:leave>Saved</div>{/if}
<ul use:reveal={{ targets: "children" }} class="cards">…</ul>Why no dials
Numbers, not options
Duration, curve and distance are fixed for each job. Stagger and delay control when the motion starts.
Jobs grow, dials never
Cube covers entrances, exits, state changes and scroll reveals. Use another tool when you need timelines, gestures or layout animation.
Platform underneath
Web Animations API and IntersectionObserver. Nothing to polyfill, no frame loop, nothing fighting the browser.
FAQ
Why not Motion or GSAP?
They give you every dial. Cube gives you four decisions already made. Reach for them when you need a timeline, gestures or layout animation.
Does Cube have dependencies?
The core has zero runtime dependencies and uses the browser’s Web Animations API. React, Vue, Solid and Svelte are optional peer dependencies for their adapters.
Where did press go?
Press feedback is three lines of CSS: a transition on scale and :active { scale: 0.97 }. CSS also answers keyboard activation, which a pointer listener never did, so the function was retired and its place went to leave.
What about springs?
Cube uses a fixed cubic-bezier curve, not a spring solver. There are no spring options.
What happens with reduced motion?
Opacity changes stay; translation, scale and blur go. rise and leave become fades, morph a crossfade. reveal applies the reduced-motion preference when an element enters view.