Skip to main content

Why hover animations became one shared library for boxes and buttons (and where the tab sits)

· 4 min read
Jon-Michael Lastimosa
Senior Web Developer

The question: We had just re-ordered the Component Presets tabs into a design-system cascade (tokens → containers → components → decoration), which put Box Presets before Buttons. The user-authored Hover Animations list (Pulse Ring, Swing, Rubber Band, …) lived inside the Buttons tab — so the effects a box might want sat behind the tab that came after it. Move the list to Box Presets? Show it in both tabs, kept in sync? Or is a list that updates "both places at once" a bad idea?

Context

Two hover-effect systems existed side by side:

  • Buttons had the built-in .btnfx-* effects (hover-fx.css, ~35 motion-only classes) plus the user list (button_animations, each emitting .btnfx-c-{slug}), both offered by the button-hover-animation picker. The effect is an extra class on the button element.
  • Box Presets had a small, separate hover_fx multi-select (lift / zoom media / tilt / glow / shine) emitted as inline declarations on .boxp-{slug}.

The built-in CSS was already element-agnostic (.btnfx-lift:hover, not .btn.btnfx-lift), and the user list's only button-specific part was the {{BTN}} token name. The Site Converter had also just gained a fingerprint that maps a captured hover (translateY → Lift, scale → Grow, …) onto those presets, with a fidelity guard — and cards needed the same mapping.

Options considered

  • Move the list to Box Presets. Pro: it's next to the first thing that hovers. Con: just relocates the asymmetry — buttons would then depend on a box-owned list.
  • Render the same list inside both tabs. Pro: discoverable from either place. Con: every Component Presets panel posts through one form, so one option id in two panels double-submits and clobbers itself; and two places to edit one thing is exactly the confusion a library avoids.
  • One shared library both consume (chosen). Pro: it is how Color Presets already work — one list, many consumers; add an animation once and every picker offers it. Con: a new tab, a storage-key move, and the box side needs a way to apply an effect without an extra class on the element.
  • Where the tab goes. After Spacing (a token library) vs. after Section Styles (right before Box Presets). A section never hovers; hover starts at the box, then the button — so the library sits where hover starts.

Decision

One shared Hover Animations library, in its own Components tab placed after Section Styles and before Box Presets, consumed by both the Button picker and a new Box Preset field. Concretely:

  • Storage moves to hover_animations; the legacy button_animations key is read as a fallback so existing sites keep their entries. Entries use {{SELECTOR}} + {{ANIM}}; {{BTN}} stays an alias.
  • Box Presets gain hover_animation — the same button-hover-animation picker, same choices (sc_get_hover_animation_choices()), listing built-ins and customs. The older box-only hover_fx composites stay as "Extra Hover Effects".
  • A box applies its effect without any class on the element: the presets stylesheet re-emits the effect onto .boxp-{slug} — a built-in is cloned from hover-fx.css (every rule naming the effect class, plus the @keyframes it animates, plus a reduced-motion guard); a custom entry is re-rendered with {{SELECTOR}} = the box (unysonplus_hover_fx_css_for()).
  • The converter maps a captured card's lift/grow onto the box preset's hover_animation with the same fidelity guard buttons use (an effect that forces a hover shadow is only used when the source has no resting shadow to lose or its hover changes the shadow).

Why

"Automatically updates both places" is the point of a library, not a hazard — the hazard is duplicated state, which the mirrored-tabs option would have created. Tying the apply-mechanism to the preset's own class (rather than adding btnfx-* to every card) keeps the box model consistent with how every other Box Preset trait works: the preset owns it, the markup stays clean, and the converter's "preset owns the appearance" rule extends to motion. Placing the tab at the start of the hover cascade (box → button) matches the design-system order the whole Components tab now follows.