Skip to main content

Lottie Animations

Drift can play Lottie animations natively using Skia's Skottie renderer. Lottie files are JSON-based motion graphics exported from After Effects that render at any resolution without quality loss.

Loading Animations​

Load a Lottie JSON file before passing it to a widget. Three loaders are available:

import (
"github.com/go-drift/drift/pkg/lottie"
)

// From an io.Reader (asset filesystem, HTTP body, etc.)
f, err := assetFS.Open("assets/bouncing-ball.json")
if err != nil {
return nil
}
defer f.Close()
anim, err := lottie.Load(f)

// From raw bytes
anim, err := lottie.LoadBytes(jsonBytes)

// From a file path
anim, err := lottie.LoadFile("/path/to/animation.json")

All three return a *lottie.Animation that holds the parsed animation data and exposes Duration() and Size() for intrinsic dimensions.

Basic Playback​

The Lottie widget plays automatically when mounted. Use the Repeat field to control what happens when playback reaches the end:

// Play once and stop at the last frame
widgets.Lottie{
Source: anim,
Width: 200,
Height: 200,
}

// Loop continuously
widgets.Lottie{
Source: anim,
Width: 200,
Height: 200,
Repeat: widgets.LottieLoop,
}

// Bounce (play forward, then reverse, continuously)
widgets.Lottie{
Source: anim,
Width: 200,
Height: 200,
Repeat: widgets.LottieBounce,
}

Completion Callback​

In play-once mode (the default), use OnComplete to run code when the animation finishes:

widgets.Lottie{
Source: anim,
Width: 200,
Height: 200,
OnComplete: func() {
s.SetState(func() {
s.showResult = true
})
},
}

OnComplete is only called in LottiePlayOnce mode and is ignored when a Controller is set.

Sizing​

The widget supports three sizing strategies:

Explicit dimensions: both Width and Height are set.

widgets.Lottie{Source: anim, Width: 300, Height: 200}

One dimension with aspect ratio preserved: set one dimension and leave the other at zero. The missing dimension is calculated from the animation's intrinsic aspect ratio.

widgets.Lottie{Source: anim, Width: 300} // Height derived from aspect ratio

Intrinsic size: leave both at zero. The widget uses the animation's native dimensions.

widgets.Lottie{Source: anim, Repeat: widgets.LottieLoop}

Programmatic Control​

For play/pause/restart behavior, pass an external AnimationController. When a controller is provided, the widget does not auto-play and ignores Repeat and OnComplete. The controller's Value (0.0 to 1.0) maps directly to animation progress.

type playerState struct {
core.StateBase
controller *animation.AnimationController
anim *lottie.Animation
}

func (s *playerState) InitState() {
// Load animation (in practice, handle errors)
f, _ := assetFS.Open("assets/bouncing-ball.json")
defer f.Close()
s.anim, _ = lottie.Load(f)

// Create a controller matching the animation duration
s.controller = animation.NewAnimationController(s.anim.Duration())
core.UseDisposable(&s.StateBase, s.controller)

// Rebuild on every frame
core.UseListenable(&s.StateBase, s.controller)

// React to completion
s.controller.AddStatusListener(func(status animation.AnimationStatus) {
if status == animation.AnimationCompleted {
// Animation reached the end
}
})
}

func (s *playerState) Build(ctx core.BuildContext) core.Widget {
return widgets.Column{
Children: []core.Widget{
widgets.Lottie{
Source: s.anim,
Controller: s.controller,
Width: 200,
Height: 200,
},
widgets.Row{
Children: []core.Widget{
theme.ButtonOf(ctx, "Play", func() {
s.controller.Forward()
}),
theme.ButtonOf(ctx, "Pause", func() {
s.controller.Stop()
}),
theme.ButtonOf(ctx, "Restart", func() {
s.controller.Reset()
s.controller.Forward()
}),
},
},
},
}
}

Lottie animations contain their own easing curves baked into keyframes, so the controller uses linear interpolation by default. There is no need to set a curve.

tip

UseDisposable is documented in the State Management guide. For animation curves and the controller API, see Animation.

Next Steps​

  • Animation for the animation controller and curves system
  • Image & SVG for raster and vector image widgets