Skip to content

Animation for developers

Getting started

How the module animation works under the hood, for anyone who customizes the theme or adds animation to a new module. Buyers who only want to use it can stay with Add animation to a module.

The pieces

File Role
js/plugin/_gsap.js The GSAP core library, self hosted.
js/plugin/_animate.js Our script. It finds modules marked for animation, reads their data-anim-* attributes and plays one GSAP timeline per module when it scrolls into view.

A module loads both with require_js and { defer: true }, and only when its Add animation to module box is ticked. Load GSAP first, then _animate.js.

{% if module.style.add_animation %}
  {{ require_js(get_asset_url("../../js/plugin/_gsap.js"), { defer: true }) }}
  {{ require_js(get_asset_url("../../js/plugin/_animate.js"), { defer: true }) }}
{% endif %}

The script does nothing when GSAP is missing, when IntersectionObserver is missing, or when the visitor prefers reduced motion. It plays each module when about 10 percent of it is in view, a little above the bottom edge.

How a module opts in

Put the settings on the module wrapper as data-anim-* attributes, and mark the elements to animate.

Wrapper attributes

Attribute Meaning
data-anim The mode: blocks, toc, or an effect name for the parts mode (banners).
data-anim-effect Effect for each block (the card, item or row). none leaves the block still.
data-anim-sub-effect Effect for the parts inside a group.
data-anim-duration, data-anim-distance, data-anim-ease Timing, travel in pixels, and smooth, soft, snappy or bounce.
data-anim-stagger, data-anim-sub-stagger Seconds between blocks, and between parts.
data-anim-once true plays once, false replays on every scroll back in.
data-anim-on-slide true plays a banner slider's text again on each slide change.
data-anim-ready Set by the script when it takes over. Do not set it yourself.

Element marks

Mark Meaning
data-anim-item One block that plays on its own (a card, a row, a media column).
data-anim-item-effect Overrides the effect for this one block.
data-anim-sub-children The direct children of this element are the parts of a group.
data-anim-title The title, split into words when the effect is Title words.
data-anim-parts, data-anim-aside, data-anim-card Parts mode: the text column, the media column, and a card box around them.
data-anim-skip Leave this child out of the group (an invisible full card link, for example).

The effects

fade_up, fade_in, zoom_in, blur_reveal, slide_left, slide_right and, for titles, words. The older slide_in still works and means from the right for blocks and media. One shared helper in _animate.js defines how each effect moves, so a name means the same thing in every mode.

Add animation to a new module

  1. Fields. In the style group add a boolean add_animation (default off) and a group animation_settings on the STYLE tab, visible only when add_animation is on. Copy both from a finished module such as Features, then rename the labels. Put both in the style group's default too, and keep the group default equal to the style default.
  2. Hide until ready. In the module's scoped CSS, hide the animated parts only when the script has not started, with a fallback that shows them anyway:
@media (scripting: enabled) {
  .my-module[data-anim]:not([data-anim-ready]) [data-anim-item] {
    opacity: 0;
    animation: my-module-reveal 0s linear 2.5s forwards;
  }
}
@keyframes my-module-reveal { to { opacity: 1; } }
  1. Attributes. Add an animation_attrs() macro that prints the data-anim-* values from animation_settings, and call it on the wrapper.
  2. Marks. Add data-anim-item (and data-anim-sub-children where a content effect applies) to the elements, only when add_animation is on.
  3. Scripts. Load GSAP and _animate.js at the end, as shown above.
  4. Check that every visibility path in the group resolves, that every choice value is one the script knows, and that the module still looks right with the box unticked.

Rules we learned

  • Never scale an element that holds a slider. The slider measures itself with its bounding box, and a scale makes it too small for good. The script moves it up instead.
  • A slider animates as one block or by its text, never card by card. Hidden cards that fade in at the edge of a slider look broken.
  • Switch CSS transitions off on animated parts. A transition on opacity or transform smears every frame. The script does this and removes it again afterwards.
  • A CSS animation beats a GSAP tween on the same element. If a part already has a CSS animation (the Tab panel switch), animate an inner child instead.
  • Build a fresh settings object for every GSAP call. GSAP changes the object it is given, so one shared copy goes stale.
  • A timeline's own delay is ignored by play(). Use a delayed call, and keep stagger under 1.5 seconds.
  • Loop slides are copies. A copy made while its text was hidden stays hidden, so the script clears every copy that is added later.
  • A highlighted word is a span inside the title. When the effect is Title words, the highlight style rule must skip the word wrappers with :not(.anim-word):not(.anim-word__inner).
  • Saved pages lack new fields. A field added later is missing on pages saved before it existed, so the template must read a missing value as its default.
  • Copied fields keep the old module's visibility paths. HubSpot rejects the upload if a path points at a field that does not exist, so check every path.

Testing

  • Test on the page preview or live page. A background browser tab pauses animation frames.
  • Check each effect with Fade in (no travel), Fade up (travel) and Zoom in (scale) and make sure the content is clean afterwards, with no leftover inline opacity, transform or filter.
  • On sliders, change slides quickly, and loop from the last slide to the first.
  • Turn on reduced motion in your device settings and confirm the page shows normally.