Declarative animations for Phoenix LiveView, powered by the Web Animations API.
Enguia lets you add smooth, performant animations to your LiveView templates using a clean Elixir DSL. No CSS files. No JavaScript boilerplate. Just functions.
<.motion animate={slide_up(delay: 100)} tag="section">
<h1>Slides in when scrolled into view</h1>
</.motion>
<.motion animate={typewriter()} tag="p">
Hello, world!
</.motion>
- 11 motion presets — fade, slide, scale, shake, pulse, bounce
- 4 text effects — typewriter, split words, blur in, letter spacing in
- 4 trigger modes —
mount,visible(scroll),hover,click - Scroll reveal — re-animate every time an element enters the viewport
- Fully composable — override duration, delay, easing, fill, repeat per call
- ~3 KB JavaScript hook, zero runtime dependencies
Add enguia to your mix.exs:
def deps do
[
{:enguia, "~> 0.1"}
]
endIn assets/js/app.js, import and register the hook:
import EnguiaHook from "../../deps/enguia/priv/static/enguia.js"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { EnguiaHook }
})Import everything with use Enguia in your LiveView or component module:
defmodule MyAppWeb.PageLive do
use MyAppWeb, :live_view
use Enguia
def render(assigns) do
~H"""
<.motion animate={fade_in()}>
<p>Fades in on mount</p>
</.motion>
<.motion animate={slide_up(delay: 100)} tag="section" class="hero">
<h1>Slides up when scrolled into view</h1>
</.motion>
<.motion animate={bounce()} tag="button">
Bouncing button
</.motion>
"""
end
endOr import selectively:
import Enguia.Components
import Enguia.PresetsAll presets accept an optional keyword list to override defaults.
| Function | Default trigger | Description |
|---|---|---|
fade_in/1 |
:mount |
Fade from 0 to 1 opacity |
fade_out/1 |
:mount |
Fade from 1 to 0 opacity |
slide_up/1 |
:visible |
Slide up from below |
slide_down/1 |
:visible |
Slide down from above |
slide_left/1 |
:visible |
Slide in from the left |
slide_right/1 |
:visible |
Slide in from the right |
scale_in/1 |
:mount |
Scale from 80% to 100% |
scale_out/1 |
:mount |
Scale from 100% to 80% |
shake/1 |
:mount |
Horizontal shake (attention grabber) |
pulse/1 |
:mount |
Infinite opacity pulse |
bounce/1 |
:mount |
Infinite vertical bounce |
fade_in(duration: 500, delay: 100, easing: "ease-out")
slide_up(trigger: :click, repeat: 2)
pulse(repeat: :infinity) # default for pulseImport from Enguia.TextAnimations (included via use Enguia):
| Function | Description |
|---|---|
typewriter/1 |
Reveals text character by character |
split_words/1 |
Animates each word in with a stagger |
blur_in/1 |
Fades text in from blurred to sharp |
letter_spacing_in/1 |
Animates from wide letter-spacing to normal |
<.motion animate={typewriter(duration: 1500)} tag="p">
One character at a time.
</.motion>
<.motion animate={split_words(stagger: 60)} tag="h2">
One word at a time.
</.motion>
<.motion animate={blur_in()} tag="h1">
Fades in from blur.
</.motion>
<.motion animate={letter_spacing_in()} tag="h1">
Tracks in from wide spacing.
</.motion>
| Trigger | When it fires |
|---|---|
:mount |
Immediately when the component mounts |
:visible |
When the element enters the viewport (IntersectionObserver) |
:hover |
On mouse enter |
:click |
On click |
By default, :visible animations fire once. Pass scroll_reveal: true to re-animate every time the element enters the viewport:
<.motion animate={slide_up(scroll_reveal: true)}>
Animates each time you scroll past it.
</.motion>
All presets accept these keyword options:
| Option | Type | Description |
|---|---|---|
duration |
integer | Duration in milliseconds |
delay |
integer | Delay before starting, in milliseconds |
easing |
string | Any CSS easing value ("ease", "ease-out", "linear", etc.) |
fill |
string | Fill mode: "forwards", "backwards", "both", "none" |
trigger |
atom | :mount, :visible, :hover, or :click |
repeat |
integer or :infinity |
Number of iterations |
scroll_reveal |
boolean | Re-animate on each viewport entry (:visible only) |
Build animations from scratch using %Enguia.Animation{}:
alias Enguia.Animation
anim = %Animation{
keyframes: [
%{"transform" => "rotate(0deg)"},
%{"transform" => "rotate(360deg)"}
],
duration: 1000,
easing: "linear",
repeat: :infinity,
trigger: :mount
}
<.motion animate={anim}>...</.motion>
| Attribute | Type | Default | Description |
|---|---|---|---|
animate |
Animation.t() |
required | The animation struct |
tag |
string | "div" |
HTML tag to render |
class |
string | nil |
CSS class |
id |
string | auto-generated | Element ID |
Any other attributes are passed through to the element.
MIT — see LICENSE for details.