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¶
- Fields. In the
stylegroup add a booleanadd_animation(default off) and a groupanimation_settingson the STYLE tab, visible only whenadd_animationis on. Copy both from a finished module such as Features, then rename the labels. Put both in the style group'sdefaulttoo, and keep the group default equal to the style default. - 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; } }
- Attributes. Add an
animation_attrs()macro that prints thedata-anim-*values fromanimation_settings, and call it on the wrapper. - Marks. Add
data-anim-item(anddata-anim-sub-childrenwhere a content effect applies) to the elements, only whenadd_animationis on. - Scripts. Load GSAP and
_animate.jsat the end, as shown above. - 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
spaninside 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,transformorfilter. - 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.