Slik
Slik is a binder-first, Motion-inspired animation framework for Leptos.
It is built around a small numeric motion core with three public layers:
use_motionfor binding animation directly to typedNodeRefsslik::html::*for thin motion-enabled HTML component sugarMotionValuefor low-level animated scalar state
v0.2 is a clean rewrite over the earlier wrapper-first MVP. This README documents the current API only.
Why Slik exists
Leptos already gives you precise reactive control over state and DOM ownership. Slik focuses on the missing piece: making motion feel native to that model.
The design goals are intentionally narrow:
- Leptos-first reactivity
- binder-first architecture instead of wrapper-first architecture
- explicit numeric motion props
- spring, tween, and keyframe transitions
- per-property transition overrides
- real browser runtime on
wasm32 - clean native checking and testing outside the browser
If you are familiar with motion.dev, the mental model is similar: declare a
target, and Slik animates from the current value to the new one. The difference
is that v0.2 is grounded in typed Leptos NodeRef binding rather than a
wrapper-centric component runtime.
What Slik is
Slik is a small, focused motion system for Leptos apps.
It is good at:
- entry animations
- hover and press interactions
- fades, lifts, slides, scale, and rotation
- per-property motion tuning
- interruptible numeric keyframes
- animated counters, meters, and dashboards
- binding motion to real HTML and SVG nodes
What Slik is not
v0.2 is intentionally not trying to cover the full Motion surface area.
It does not currently include:
- variants
- exit / presence orchestration
- gesture APIs
- layout animation
- color interpolation
- arbitrary CSS string interpolation
- SVG sugar components
- a generic catch-all motion element API
That scope is intentional. v0.2 is the architectural foundation.
At a Glance
| Layer | Use it when | Main API |
|---|---|---|
| Binder | You want maximum control on a real node | use_motion, MotionOptions |
| Sugar | You want ergonomic motion-enabled HTML components | slik::html::* |
| Scalar | You want a single animated f64 |
MotionValue |
Installation
If you are using Slik from this workspace or before a public crates.io release:
[]
= { = "crates/slik", = ["csr"] }
= { = "0.8", = ["csr"] }
Slik mirrors Leptos target features:
csrhydratessr
Pick the same feature set you use for leptos.
Quick Start
The ergonomic starting point is usually the HTML sugar layer.
use *;
use MotionDiv;
use *;
This mounts with:
opacity: 0 -> 1translateY: 24px -> 0px
using a tweened opacity + transform target on a real <div>, with no extra
wrapper inserted by Slik.
The v0.2 Mental Model
Slik v0.2 is easiest to understand through four concepts.
1. MotionStyle
MotionStyle is the sparse target definition for supported motion properties.
new
.opacity
.x
.scale
Each property is optional. If you do not set a property, it is absent from that style snapshot.
2. Transition
Transition defines how a value moves to its target.
Slik ships with three transition families:
Transition::spring()Transition::tween(duration_secs, easing)Transition::keyframes(keyframes, duration_secs)
3. use_motion
use_motion is the canonical v0.2 API.
It binds motion to a typed Leptos NodeRef, owns the target node's inline
opacity, transform, and will-change, and keeps them synced to reactive
motion values.
4. MotionValue
MotionValue is the low-level scalar primitive.
Use it when you want animation without binding a full MotionStyle to a DOM
node.
Binder-First Example
The binder is the foundation. Sugar components are layered on top of it.
use *;
use *;
This is the core of the library:
- you create a real
NodeRef - you describe
initialandanimate - Slik manages the motion runtime
- your element remains your element
No polymorphic wrapper, no hidden extra node.
MotionHandle
use_motion returns a MotionHandle.
That handle exposes dense per-property MotionValues:
let handle = use_motion;
let x_value = handle.values.get;
This is useful when you want binder-driven DOM motion plus direct access to one or more animated scalar channels.
Thin HTML Sugar
The HTML sugar layer exists for ergonomics, not as a separate architecture.
Every motion HTML component is a thin wrapper over use_motion.
use *;
use MotionButton;
use *;
Important attribute forwarding note
Motion HTML components use Leptos AttributeInterceptor.
That means:
- event handlers such as
on:clickwork normally - plain DOM attributes should be forwarded with
attr:*
Examples:
<MotionDiv attr:style="padding:1rem" attr:id="hero-card">
"..."
</MotionDiv>
Available Motion HTML Components
Current sugar coverage includes:
- Structure:
MotionArticle,MotionAside,MotionDiv,MotionFooter,MotionHeader,MotionMain,MotionNav,MotionSection - Headings:
MotionH1,MotionH2,MotionH3,MotionH4,MotionH5,MotionH6 - Text:
MotionBlockquote,MotionCode,MotionEm,MotionP,MotionPre,MotionSmall,MotionSpan,MotionStrong - Lists:
MotionDl,MotionLi,MotionOl,MotionUl - Forms and controls:
MotionButton,MotionDetails,MotionFieldset,MotionForm,MotionLabel,MotionLegend,MotionSummary,MotionTextarea - Other content:
MotionA,MotionFigcaption,MotionFigure
If you need an element that does not yet have sugar, the binder layer is already more general than the current macro list.
Supported Motion Properties
Slik currently supports these numeric motion 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
Transforms are composed in this fixed order:
translateX -> translateY -> scale -> rotate
Axis-specific scale multiplies the uniform scale:
effective_scale_x = scale * scale_x
effective_scale_y = scale * scale_y
So this:
new.scale.scale_x
produces:
scale(0.96, 1.2)
initial vs animate
initial
initial seeds the starting visual state for owned properties.
animate
animate is the live target snapshot.
Example:
<MotionDiv
initial=derive
animate=new.opacity.y
>
"Fade + slide on mount"
</MotionDiv>
If initial is omitted, the first animate snapshot becomes the seed state.
That means:
- no mount transition unless
initialdiffers fromanimate - the binder starts from the first known target by default
Property ownership semantics
Once a property appears in either initial or animate, that property becomes
owned by the binding.
In practice this means:
- Slik keeps writing that property's current animated value
- omitting a previously owned property from a later
animatesnapshot does not implicitly hand control back to other inline styles
This is deliberate. It avoids ambiguous DOM ownership after motion has claimed a property.
Reactive Targets
Reactive motion targets are a natural fit for Leptos signals and memos.
use *;
use MotionDiv;
use *;
When the signal changes, Slik retargets from the current sampled value.
Transitions
Slik has three transition families.
Spring
The default transition is a spring.
spring
Included presets:
spring
spring_bouncy
spring_gentle
spring_custom?
Use springs for:
- hover interactions
- press feedback
- panels and cards
- UI motion that should feel responsive and physical
spring_custom is validated and returns Result<Transition, TransitionError>.
Tween
Tweens run for a fixed duration with cubic-bezier easing.
let tween = tween?;
Available easings:
Linear
Ease
EaseIn
EaseOut
EaseInOut
Snappy
Custom
Use tweens for:
- fades
- deterministic micro-interactions
- motion where a fixed duration matters more than spring feel
tween is validated and returns Result<Transition, TransitionError>.
Keyframes
Keyframes are literal numeric sequences over normalized progress from 0.0 to
1.0.
Each keyframe has:
- an
offset - a
value - an easing used for the segment that ends at that keyframe
Helpers:
current
absolute
target
Example:
let pulse = keyframes?;
This means:
- start from the live current value
- overshoot to
1.24 - settle toward
1.06 - finish at the requested target
Keyframe 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 keeps retargeting semantics explicit and interruptible.
Per-Property Transitions
Use TransitionMap when different properties should move differently.
let transitions = new
.with
.with;
This lets you do things like:
- spring translation
- tween opacity
- snap rotation differently from position
Example:
use *;
use MotionDiv;
use *;
Reduced Motion
Slik exposes both system-level reduced-motion state and per-binding policy.
Browser preference signal
let prefers_reduced_motion = use_reduced_motion;
This mirrors the browser's prefers-reduced-motion media query.
Binding policy
ReducedMotionConfig controls how a specific motion binding responds:
ReducedMotionConfig::AutoReducedMotionConfig::AlwaysReducedMotionConfig::Never
Example:
<MotionDiv
animate=target
transition=new
reduced_motion=Always
attr:style="width:48px; height:48px; background:#111827; border-radius:12px"
/>
Always forces immediate jumps. Auto follows the browser preference. Never
ignores it.
Low-Level API: MotionValue
MotionValue animates a single f64.
Use it when:
- you want animated numeric state without DOM binding
- you want to drive text, counters, or readouts
- the binder layer is too high-level for the job
Example:
use *;
use *;
MotionValue methods
new
value.get
value.get_untracked
value.target
value.is_animating
value.is_animating_untracked
value.set_target
value.jump
value.stop
value.signal
Semantics
set_target(v)animates from the current sampled value towardv- spring retargeting preserves spring momentum
- tween and keyframe retargeting restart from the current sampled value
jump(v)snaps immediately and clears active animationstop()halts animation at the current sampled value
DOM Behavior
No wrapper node
This is the most important v0.2 shift.
Slik no longer centers the library around a dedicated wrapper component. The binder targets real nodes directly, and the HTML sugar components simply create those nodes for you.
HTML and SVG binding
use_motion supports style-capable HTML and SVG nodes.
That means:
- HTML binding is covered by both
use_motionandslik::html::* - SVG binding is available through the binder layer today
- HTML sugar is explicit; SVG sugar does not exist yet
DOM properties owned by Slik
The binder writes:
opacitytransformwill-change
will-change is only emitted for actively animating groups:
opacitytransform
It is not left on permanently after animation settles.
Browser and Native Runtime Behavior
Slik is designed to be pleasant in a mixed Rust + wasm workflow.
In practice:
- the real animation loop runs in the browser on
wasm32 - the browser runtime is driven by
requestAnimationFrame - native
cargo check,cargo test, and workspace builds remain straightforward - on non-wasm targets, animation requests snap immediately to the latest target
This is deliberate. It keeps the browser behavior correct without making native tooling awkward.
API Overview
slik::prelude::*
The prelude re-exports the main working surface:
| Item | Purpose |
|---|---|
use_motion |
binder-first motion hook |
use_reduced_motion |
browser reduced-motion signal |
MotionOptions |
binder configuration |
MotionHandle |
handle returned by use_motion |
MotionValues |
dense per-property motion values |
ReducedMotionConfig |
per-binding reduced-motion policy |
MotionStyle |
sparse motion target definition |
MotionProp |
explicit property enum |
MotionValue |
low-level animated scalar |
Transition |
spring, tween, keyframe families |
TransitionMap |
default + per-property transitions |
TransitionError |
spring/tween validation errors |
Easing |
easing presets and custom bezier |
Keyframe |
keyframe builder |
KeyframeValue |
current / absolute / target keyframe values |
KeyframeTransition |
validated keyframe sequence |
KeyframeError |
keyframe validation errors |
slik::html
Use this module when you want motion-enabled HTML components such as:
MotionDivMotionButtonMotionSectionMotionMainMotionPMotionH1throughMotionH6
Current Limitations
The current scope is intentionally lean.
Notable constraints today:
- numeric properties only
- no layout animation
- no color interpolation
- no arbitrary CSS string interpolation
- no exit / presence orchestration
- no variants
- no SVG sugar components
- sugar coverage is explicit, not open-ended
These are reasonable next-step candidates, but they are not required to use the current v0.2 foundation effectively.
Design Choices in v0.2
These choices are deliberate.
Binder-first, sugar-second
use_motion is the architectural center.
The sugar layer exists because ergonomics matter, but it is intentionally a thin layer over the binder instead of a competing runtime.
Numeric-only motion surface
All current motion values are f64.
That keeps interpolation simple, predictable, and easy to test.
Explicit property enum
Per-property overrides use MotionProp, not string keys.
That avoids typo-driven configuration mistakes.
Keyframe target is explicit
The final keyframe must target KeyframeValue::Target.
That keeps interrupt and retarget behavior coherent.
Showcase
The repository includes a working showcase in:
examples/showcase/src/main.rs
It demonstrates:
- binder-first entry animation
- HTML sugar components
- tween targets
- keyframe pulse
MotionValuecounters- per-property overrides
- reduced-motion policy
- an interactive
MotionButton
Status
Slik v0.2 is the first real public-facing foundation for the crate.
It is intentionally narrow, but the pieces that are present are meant to be coherent:
- typed DOM binding
- thin ergonomic sugar
- explicit motion properties
- predictable numeric interpolation
- validated transition inputs
- browser-aware reduced-motion handling
- a low-level primitive when the binder layer is too high-level
If you understand this README, you understand the current shape of the crate.
License and Contributions
MIT.
Contributions are currently not being actively solicited while the roadmap from v0.2 toward v1.0 is still being shaped.
Issues, forks, and local experimentation are welcome.