Declarative UI animations for Phoenix LiveView — entrance/exit, gestures, springs, and page transitions, straight from HEEx with (almost) no JavaScript.

Add one <.motion> component to your HEEx and get entrance/exit animations, hover/tap/drag gestures, spring physics, scroll triggers, layout (FLIP) animations, and server-driven page transitions. Animations run on the browser's Web Animations API for GPU-accelerated performance; the Elixir side stays purely declarative.

<.motion id="hero" animate="slide-up" hover="lift">
  <h1>Hello, world!</h1>
</.motion>

Why LiveAnimate

  • Declarative from HEEx. Describe the animation on the element — no hand-written JavaScript, no imperative transition code.
  • Built for the LiveView lifecycle. Exit animations that play before the server removes an element, FLIP layout animations that survive stream reflows and reorders, and whole-page transitions you declare on a LiveView with @transition.
  • Rich motion out of the box. Spring physics, velocity-aware drag, staggered lists, scroll/viewport triggers, and more than two dozen presets — plus your own inline keyframes and named variants.
  • No runtime npm dependency. The animation engine is hand-rolled and ships with the package; setup is one hook and one CSS import.
  • Accessible by default. Honors prefers-reduced-motion, with a per-element opt-out for motion you consider decorative.

Compared to JS.transition

Phoenix ships Phoenix.LiveView.JS.transition, which toggles CSS classes — you hand-write the keyframes and CSS yourself, and there's no spring physics, gestures, drag, FLIP layout, or page transitions. LiveAnimate gives you all of that declaratively, so most UI motion becomes a preset name instead of a stylesheet plus a JS command.

Installation

1. Add the dependency

# mix.exs
def deps do
  [
    {:live_animate, "~> 0.1.0"}
  ]
end

2. Set up JavaScript

// assets/js/app.js
import LiveAnimate from "live_animate"

let liveSocket = new LiveSocket("/live", Socket, {
  ...LiveAnimate.config(),
  hooks: { ...LiveAnimate.hooks },
  params: { _csrf_token: csrfToken }
})

LiveAnimate.init()

3. Import CSS

/* assets/css/app.css */
@import "../../deps/live_animate/assets/css/live_animate.css";

4. Import the component

# lib/my_app_web.ex
defp html_helpers do
  quote do
    import LiveAnimate
    # ...
  end
end

Quick start

<%# Fade in on mount %>
<.motion id="hero" animate="fade">
  <h1>Hello, world!</h1>
</.motion>

<%# Hover and tap interactions %>
<.motion id="btn" hover="scale-up" tap="press">
  <button>Click me</button>
</.motion>

<%# Exit animation %>
<.motion id="toast" animate="slide-left" exit="fade">
  <p>Dismissible toast</p>
</.motion>

Available presets

PresetTypeDescription
fadeentrance/exitOpacity 0 to 1
blurentrance/exitOpacity + blur
slide-upentrance/exitTranslate Y + fade
slide-downentrance/exitTranslate Y + fade
slide-leftentrance/exitTranslate X + fade
slide-rightentrance/exitTranslate X + fade
zoom-inentrance/exitScale 0.8 to 1 + fade
zoom-outentrance/exitScale 1.2 to 1 + fade
dropentrance/exitTranslate Y + scale + fade
flip-xentrance/exitRotate X 90deg + fade
flip-yentrance/exitRotate Y 90deg + fade
scale-upgestureScale to 1.05
scale-downgestureScale to 0.95
pressgestureScale to 0.92
liftgestureScale + drop shadow
tilt-leftgestureRotate -3deg
tilt-rightgestureRotate 3deg
shakekeyframeHorizontal shake
bouncekeyframeVertical bounce
pulsekeyframeScale pulse
wigglekeyframeRotational wiggle
spinkeyframeFull 360deg rotation
pingkeyframeScale up + fade out
rubber-bandkeyframeElastic stretch
floatkeyframeGentle vertical bob (pair with loop: true)
highlightkeyframeNon-displacing attention pulse (ring + tint)

Browser support

  • Core animations (entrance/exit, gestures, springs, keyframes) use the Web Animations API and individual transform properties — supported across current Chromium, Firefox, and Safari.
  • Page transitions use the View Transitions API (Chromium-based browsers and Safari 18+). Where it isn't available, navigation falls back to an instant swap with no error.
  • All animations respect the user's prefers-reduced-motion setting and collapse to instant when it's enabled — override per element with respect_motion={false}.

Documentation

Full documentation is available on HexDocs.

License

MIT - see LICENSE.