Scroll Animations Setup


How to add scroll-triggered animations to the site — both vector Lottie animations and canvas frame-sequence scrubbers (the pin-and-scroll “shiny cat” effect). Built directly on the hugo-theme-console theme, no Hinode needed.

Files Included

  1. layouts/shortcodes/lottie.html — {{< lottie >}} shortcode (vector animations)
  2. layouts/shortcodes/frameseq.html — {{< frameseq >}} shortcode (frame sequences / rendered clips)
  3. static/js/lottie_light.min.js — self-hosted Lottie player (SVG, light build)
  4. static/js/lottie-scroll.js — play-on-view / scrub logic for Lottie
  5. static/js/frame-scrub.js — canvas frame-sequence scrubber
  6. layouts/partials/footer.html — lazy-loads the scripts per page

First: pick the right tool

There are two very different kinds of “Lottie,” and using the wrong one is what causes flickering/garbled playback.

Your animation is…UseWhy
Vector (shapes exported from After Effects via Bodymovin){{< lottie >}}Small, crisp, scales infinitely
A rendered clip (3D render, video, image2lottie export){{< frameseq >}}It’s really a frame sequence — canvas draws each frame with zero flicker

The image2lottie trap: its “Lottie JSON” export is not a vector animation — it’s 400+ full-frame images stacked as layers (a flipbook). The Lottie SVG renderer flickers badly on it. Do not use the JSON. Export the WebP Sequence (“high quality frames”) instead and feed it to frameseq.

Vector Lottie — {{< lottie >}}

Step 1: Export from After Effects

Use the Bodymovin plugin (or LottieFiles) to export a .json. Keep it to shapes/transforms/opacity/masks — the light player is SVG-only, so AE expressions and effects won’t render.

Step 2: Drop the file in /static

static/
└── my-anim.json

Step 3: Use it in any page

{{< lottie src="/my-anim.json" >}}                        <!-- play once when it scrolls into view -->
{{< lottie src="/my-anim.json" mode="scrub" >}}           <!-- timeline follows scroll position -->
{{< lottie src="/my-anim.json" loop="true" height="360px" >}}

Params: src (required), mode (play | scrub), loop (true/false), height, class.

Frame sequence — {{< frameseq >}} (the pin-and-scroll effect)

Step 1: Get the frames

Export a WebP Sequence from image2lottie (or any numbered image sequence), named frame_000.webp, frame_001.webp, … in one folder.

Step 2: Drop the folder in /static

static/
└── lottie/
    └── cat-frames/
        ├── frame_000.webp
        ├── frame_001.webp
        └── … (423 frames)

Step 3: Use it in a page

{{< frameseq dir="/lottie/cat-frames" count="423" >}}                          <!-- sticky/pinned scrub (default) -->
{{< frameseq dir="/lottie/cat-frames" count="423" sticky="false" height="480px" >}}  <!-- inline scrub -->

<!-- recommended: add a low-res set for phones -->
{{< frameseq dir="/lottie/cat-frames" dir-small="/lottie/cat-frames-sm" count="423" >}}

Params:

ParamDefaultMeaning
dir— (required)Folder under /static holding the frames
count— (required)Number of frames
dir-small—Lower-resolution frame folder used on narrow screens. This is the main mobile-smoothness lever
small-max768Viewport width (px) at or below which dir-small is used
stickytruetrue pins the canvas full-height and scrubs as you scroll past; false scrubs inline
track300vhScroll distance for a full play-through in sticky mode — raise for slower scrub, lower for faster
height70vhCanvas height in inline mode
labelloadingText shown while frames download
pad3Zero-padding of the frame index (frame_000)
extwebpFrame file extension
class—Extra CSS classes

Loading behaviour

Frame sequences are heavy, so the shortcode handles the wait for you:

Making a low-res set for phones

Serving 1280×720 frames to a 390px-wide phone means decoding 4× more pixels than the screen can show — that’s what makes mobile scrubbing choppy. Generate a 640px-wide set alongside the full one:

brew install webp   # provides cwebp / dwebp

mkdir -p static/lottie/cat-frames-sm
for f in static/lottie/cat-frames/frame_*.webp; do
  base=$(basename "$f" .webp)
  dwebp -quiet "$f" -o /tmp/t.png
  cwebp -quiet -resize 640 0 -q 80 /tmp/t.png -o "static/lottie/cat-frames-sm/$base.webp"
done

Then pass it as dir-small. The resolution is chosen once on load (never swapped mid-session, which would re-download every frame).

Testing

  1. Run hugo serve -D (the -D shows draft pages)
  2. Open the lab page, e.g. http://localhost:1313/notes/scroll-scrub/
  3. Scroll — the cat pins to the viewport and spins in sync with the scrollbar

The scripts only load on pages that actually use a shortcode (guarded by .HasShortcode in footer.html), so other pages stay lightweight.

Tuning the scrub feel

Mobile notes

Troubleshooting

Flickering / garbled frames

You’re feeding an image2lottie JSON flipbook to the Lottie SVG player. Switch to {{< frameseq >}} with a WebP Sequence instead.

Animation doesn’t show up on the live site

Check the page isn’t draft: true — the GitHub Actions build runs hugo --minify without -D, so drafts are excluded from production.

Canvas is blank

Nothing loads at all

Hugo Theme Compatibility

Built for the hugo-theme-console theme. The scripts are injected via the overridable layouts/partials/footer.html. If you change themes, make sure the footer (or an equivalent end-of-<body> hook) still renders.