Declarative animation for LiveView

Your LiveView,
animated.

One component. Real spring physics. Animate enter, exit, position and size in plain HEEx — no phx-hook , no JS commands to wire up.

enter exit position size stagger spring hook-free server-driven enter exit position size stagger spring hook-free server-driven

See it move.

Scroll to explore
— 01

Enter & exit, in a grid.

Render a list; add or remove items in your LiveView. Shift animates each card in when it arrives and out when it leaves — no per-element wiring.

Add and remove items in a grid.

1
2
3
cards.heex .heex
<.animated
  :for={card <- @cards}
  id={card.id}
  initial={%{y: 16, scale: 0.95}}
  exit={%{y: -16, scale: 0.95}}
>
  {card.n}
</.animated>
— 02

Reordering with real FLIP.

Shuffle a list and Shift measures the layout delta, then animates every item from its old slot to its new one. Position is just another thing your server can change.

Add, remove, sort, or reorder — surviving items slide to their new positions via FLIP, added items fade in, removed items fade out.

Draft the hero
Style the cards
Wire animations
Polish edges
Ship landing
Write docs
reorder.heex .heex
<.animated :for={row <- @rows} id={row.id}>
  {row.label}
</.animated>
— 03

Filtering, staggered.

Type into the field — matching items animate in, non-matches drop out. Staggered enters, simultaneous exits, all driven by the filtered list on the server.

Type to filter — items animate out as they stop matching, and back in as they match.

Shift
Motion One
AutoAnimate
Framer Motion
GSAP
React Spring
Anime.js
list.heex .heex
<.animated
  :for={name <- @results}
  id={name}
  initial={%{y: 8}}
  exit={%{scale: 0.92}}
>
  {name}
</.animated>
— 04

Expand & collapse, by height.

Animate from height: 0 to height: auto without measuring or freezing the box.

With a handle. The header is the toggle. The content animates its height — the content area is part of the same visual element as the handle.

accordion.heex .heex
<div class="overflow-hidden">
  <button phx-click="toggle">Section title</button>
  <.animated
    :if={@open}
    initial={%{height: 0}}
    exit={%{height: 0}}
  >
    ...content of any height...
  </.animated>
</div>

Plain toggle. The panel doesn't exist in the DOM until you click Show details — Shift animates it in the moment LiveView renders it, no hooks or mount lifecycle to wire up. Animates back out the moment LiveView removes it.

toggle.heex .heex
<button phx-click="toggle">
  Show / Hide
</button>
<.animated
  :if={@open}
  initial={%{height: 0}}
  exit={%{height: 0}}
>
  A separate panel that slides open by animating its height.
</.animated>
— 05

Modal with a real spring.

An overlay that pops in with a spring and fades out on dismiss. The transition map is the only thing different from a regular div.

A dialog that pops in with a spring and fades out on dismiss. The transition is the only thing different from a regular div.

modal.heex .heex
<.animated
  :if={@open}
  initial={%{scale: 0.9, y: 8}}
  exit={%{scale: 0.95}}
  transition={%{type: :spring, stiffness: 300, damping: 26}}
>
  Pops in with a spring, fades out on dismiss.
</.animated>
— 06

Tune the spring.

Stiffness, damping, mass. Drag the sliders and watch the same animation change character — overshoot, settle, bounce. The runtime simulates and bakes keyframes per render.

A real spring simulation — tune its feel, then move the box. The same transition drives the FLIP slide here, and enter/exit elsewhere.

Presets
spring.heex .heex
<.animated
  transition={%{type: :spring, stiffness: 260, damping: 20}}
>
  Any animation on this element runs as a real spring simulation —
  enter, exit, position and size all share the same physics.
</.animated>
— 07

Notifications, dismissed by the server.

The LiveView schedules an auto-dismiss with Process.send_after — Shift handles the in/out animation on the client. Server stays in charge.

Toasts slide in, then auto-dismiss after a few seconds.

No notifications.

toasts.heex .heex
<.animated
  :for={toast <- @toasts}
  id={toast.id}
  initial={%{x: 48}}
  exit={%{x: 48}}
>
  {toast.text}
</.animated>
— 08

Tables, sorted in place.

Click a column header — rows FLIP to their new positions. Same <.animated> component, rendered as a <tr> via the as attribute.

Click a column header to sort — rows FLIP into their new positions. Same <.animated> component, rendered as a <tr> via the as attribute.

Brass hinge $12.50 38
Cedar plank $18.00 76
Cinder block $4.20 142
Drift magnet $31.90 9
Eclipse vial $88.00 4
table.heex .heex
<tbody>
  <.animated
    :for={row <- @rows}
    as="tr"
    id={row.id}
    exit={%{scale: 0.96}}
  >
    <td>{row.name}</td>
    <td>{row.price}</td>
    <td>{row.stock}</td>
  </.animated>
</tbody>

Three convictions.

Why Shift exists
— 01

Markup, not magic.

The whole API is one component. Render it like you'd render anything else — if you can render it, Shift can animate it.

— 02

Springs over keyframes.

Shift uses a real spring solver — stiffness, damping, mass — so interrupted, redirected and overlapping animations all stay continuous and natural.

— 03

Server stays in charge.

Your LiveView remains the source of truth. Shift never asks you to push events, install hooks, or duplicate state on the client.

Two steps. That's it.

Add the dep, import the component. Shift carries its own JS — you don't have to wire up a hook.

i.
# mix.exs
{:shift, "~> 0.1"}
ii.
// assets/js/app.js
import "shift"
iii.
# your_live.heex
<.animated id={id}> ... </.animated>