Slik
Slik is a Motion-inspired animation library for Leptos.
It gives you two layers:
- a declarative
<Motion>component for animating UI props - a low-level
AnimatedSignalprimitive for animating arbitrary numeric state
The v0.1 MVP is deliberately small and sharp:
- numeric animation only
- Leptos-first reactive API
- springs, tweens, and keyframes
- per-property transition overrides
- browser runtime driven by
requestAnimationFrame - safe lifecycle cleanup through Leptos owners
If you are familiar with motion.dev, the mental model is similar: you declare a target, and Slik animates from the current value to the new one. The keyframe model also follows Motion-style timing with normalized offsets between 0.0 and 1.0, and supports “start from current value” semantics for interruptible animations.
What Slik is
Slik is a small animation runtime for Leptos apps.
It is built for the most common UI motion needs:
- fade in / fade out
- slide and lift animations
- scale and rotate interactions
- springy reactive transitions
- keyframed micro-interactions
- animated numeric state for counters, meters, and dashboards
What Slik is not
v0.1 is not trying to cover the full Motion surface area.
It does not currently include:
- variants
- exit / presence orchestration
- gesture APIs
- SVG-specific motion components
- color interpolation
- layout animation
- animation of arbitrary CSS strings
That is intentional. v0.1 focuses on a tight, reliable numeric core.
Core mental model
Slik has four core concepts.
1. AnimProps
This is the target animation state for a Motion wrapper.
new
.opacity
.x
.scale
Each field is optional. A property is only animated if you set it.
2. Transition
This defines how a property moves to its target.
Slik ships with three transition families:
Transition::spring()Transition::tween(duration, easing)Transition::keyframes(keyframes, duration)
3. Motion
<Motion> is the declarative component.
You give it:
- an optional
initial - an
animatetarget - an optional
transition - children
When animate changes, Slik animates any owned properties to their new targets.
4. AnimatedSignal
This is the lower-level primitive.
It is useful when you want an animated numeric value without routing everything through a wrapper component.
Supported properties
v0.1 supports these numeric props:
| Property | Meaning | Units |
|---|---|---|
opacity |
CSS opacity | unitless |
x |
horizontal translation | px |
y |
vertical translation | px |
scale |
uniform scale | unitless |
scale_x |
x-axis scale multiplier | unitless |
scale_y |
y-axis scale multiplier | unitless |
rotate |
rotation | deg |
Transform composition order
Slik composes transforms in this order:
translateX -> translateY -> scale -> rotate
Axis scale multiplies the uniform scale:
effective_scale_x = scale * scale_x
effective_scale_y = scale * scale_y
So this:
new.scale.scale_x
produces:
effective x scale = 0.96
effective y scale = 1.2
Installation
Until Slik is published, use it as a workspace member or path dependency.
[]
= { = "../../crates/slik" }
Once published, this becomes the usual crates.io dependency.
Quick start
use *;
use *;
This mounts with:
opacity: 0 -> 1translateY: 24px -> 0px
using the default spring.
Using reactive targets
Leptos component props can accept reactive sources directly, and Signal<T> is the preferred modern prop wrapper for values that may be static or reactive. Slik uses that pattern for animate, so you can pass a plain AnimProps, a signal, or a memo. Optional props like class are represented with MaybeProp<T>.
use *;
use *;
When hovered changes, the memo changes, and Slik animates to the new target.
initial vs animate
initial
initial is the starting visual state.
animate
animate is the live target.
Example:
<Motion
initial=new.opacity.y
animate=new.opacity.y
>
<p>"Fade + slide on mount"</p>
</Motion>
If initial is omitted, Slik uses the first animate value as the starting state.
That means:
- no unwanted jump on mount
- controlled properties begin from the first known target
Transitions
Spring
The default transition is a spring.
let t = spring;
Included presets:
spring
spring_bouncy
spring_gentle
spring_custom
Use springs for:
- hover interactions
- panel expansion
- UI elements that should feel responsive and physical
Example
<Motion
animate=new.scale.y
transition=spring.into
>
<button>"Springy button"</button>
</Motion>
Tween
Tweens run for a fixed duration with a cubic Bézier easing.
let t = tween;
Available easings:
Linear
Ease
EaseIn
EaseOut
EaseInOut
Snappy
Custom
Use tweens for:
- fades
- deterministic micro-interactions
- deliberate UI choreography where fixed duration matters more than physical feel
Example
<Motion
animate=new.opacity.x
transition=tween.into
>
<div>"Tweened content"</div>
</Motion>
Per-property transitions
You can override the default transition per property using TransitionConfig.
use *;
let transition = new
.with
.with;
This lets you do things like:
- spring position
- tween opacity
- snappier rotation
Example
Keyframes
Slik keyframes are literal numeric keyframes.
They are defined as a sequence of keyframes over normalized progress from 0.0 to 1.0.
Each keyframe has:
- an
offset - a
value - an easing applied for the segment ending at that keyframe
Keyframe value kinds
Current
Absolute
Target
You will normally create them using helpers:
current
absolute
target
Validation rules
A keyframe transition must satisfy all of these:
- at least 2 keyframes
- first offset must be
0.0 - last offset must be
1.0 - offsets must strictly increase
- absolute values must be finite
- final keyframe must be
Keyframe::target(1.0)
That final target requirement is intentional: it makes retargeting semantics explicit and keeps the sequence interruptible.
Motion-style current value semantics
Motion supports keyframe timing through a normalized times array and allows sequences to begin from the current value. Slik mirrors that idea through offset and Keyframe::current(0.0), while making the final target explicit with Keyframe::target(1.0).
Example: pulse
use *;
let pulse = keyframes?;
This means:
- start from the live current value
- overshoot to
1.24 - settle toward
1.06 - finish at the requested target
Example: closed-loop style sequence
let bounce = keyframes?;
This is the Slik equivalent of a Motion-style multi-step sequence with custom timing.
Low-level API: AnimatedSignal
AnimatedSignal animates a single f64 value.
Use it when you want animation without a wrapper component.
Example: animated counter
use *;
use *;
Methods
new
animated.get
animated.get_untracked
animated.target
animated.set_target
animated.set_immediate
animated.signal
Semantics
set_target(v)animates from the current value tov- setting the same target again is a no-op
set_immediate(v)stops any running animation and snaps immediately tov- cleanup is automatic inside a Leptos owner via
on_cleanup
Styling and DOM behavior
<Motion> currently renders a wrapper <div> around its children.
view!
becomes conceptually:
Hello
class
You can pass a static or reactive class value:
<Motion
class="card"
animate=new.opacity
>
<div>"content"</div>
</Motion>
will-change
When Slik owns opacity and/or transform props, it emits will-change for those properties.
This helps hint browser optimization for common UI motion paths.
SSR and native cargo check behavior
Leptos effects are intended to synchronize reactive state with the outside world, and browser-only work is commonly wrapped in Effect::new() because effects run on the client. Slik follows that pattern for wiring motion updates.
In practice, that means:
- the codebase can be checked and tested natively
- the actual animation loop runs in the browser on
wasm32 - non-wasm animation starts by snapping immediately to the target instead of trying to run a browser scheduler
That split is deliberate. It keeps the crate ergonomic for workspace builds while preserving correct browser runtime behavior.
Complete example
use *;
use *;
API overview
Re-exported through slik::prelude::*
| Item | Purpose |
|---|---|
Motion |
declarative animated wrapper component |
AnimProps |
target values for supported motion props |
MotionProp |
property enum for transition overrides |
Transition |
spring, tween, and keyframe transitions |
TransitionConfig |
default + per-property transitions |
AnimatedSignal |
low-level animated numeric signal |
Easing |
tween / keyframe easing presets |
Keyframe |
keyframe builder type |
KeyframeValue |
current / absolute / target keyframe values |
KeyframeError |
validation errors for keyframes |
Design choices in v0.1
These choices are intentional:
Wrapper component, not polymorphic elements
Motion always renders a <div> in v0.1.
That keeps the internal model simple while the numeric animation core stabilizes.
Numeric-only interpolation
All supported values are f64.
This avoids the complexity of string parsing and mixed-unit interpolation in the MVP.
Explicit property enum for overrides
TransitionConfig::with(...) uses MotionProp, not string keys.
That avoids typo-driven silent failures.
Keyframe target is explicit
The final keyframe must be target.
This makes mid-flight retargeting consistent and predictable.
Limitations and roadmap candidates
Potential next steps after v0.1:
- polymorphic motion elements
MotionSpan,MotionButton, or generic element rendering- variants and orchestration
- exit / presence support
- colors and CSS variable animation
- transform origin
- SVG support
- layout animation
- gesture APIs
But none of those are required to understand or use the current MVP.
Minimal checklist for using Slik well
- use
Motionwhen animating UI wrappers - use
AnimatedSignalfor numeric state - use springs for interactive motion
- use tweens when exact duration matters
- use keyframes for multi-step motion
- start keyframes with
Keyframe::current(0.0)when you want interruptible animations - always end keyframes with
Keyframe::target(1.0) - use
TransitionConfigwhen different properties need different motion styles
Example imports
use *;
use *;
That is enough for essentially all v0.1 usage.
Status
Slik v0.1 is a focused MVP animation core for Leptos.
It is intentionally narrow, but the pieces that are present are designed to be coherent:
- reactive targets
- lifecycle-safe cleanup
- predictable numeric interpolation
- Motion-like declarative ergonomics
- a low-level primitive when the component layer is too high-level
If you read this README end to end, you should have enough context to use the current MVP comfortably.
License and Contributions
MIT!
Feel free to fork and play around with the code!
Contributions are currently not encouraged because the team is cooking up the future roadmap on how the project should shape up.
Once the roadmap is clear and we have clarity on how to go from v0.1 to v1.0, we will be happy to take contributions!