Understanding Zepto's Fx Module: Animation with CSS3 Transitions and Transforms

Overview of the Fx Module

The Fx module in Zepto enables animation support using CSS3 transitions and keyframe animations. It focuses on modern browser capabilities, relying heavily on vendor-prefixed CSS properties for compatibility. For browsers that do not support CSS3 transitions or animations, Zepto skips the animation phase entirely—styles are applied immediately, and any completion callback is invoked without delay.

This analysis is based on Zepto version 1.2.0.

Core Utility Functions

dasherize

function dasherize(str) {
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}

This utility converts camelCase strings (e.g., marginTop) into kebab-case equivalents (e.g., margin-top), aligning with standard CSS property syntax.

normalizeEvent

function normalizeEvent(name) {
  return eventPrefix ? eventPrefix + name : name.toLowerCase();
}

Adds a vendor-specific prefix to DOM event names (like transitionend), ensuring correct event binding across different rendering engines.

Detecting Browser Prefix Support

Initialization Variables

var prefix = '',
    eventPrefix,
    vendors = { Webkit: 'webkit', Moz: '', O: 'o' },
    testEl = document.createElement('div'),
    supportedTransforms = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,
    transform,
    transitionProperty, transitionDuration, transitionTiming, transitionDelay,
    animationName, animationDuration, animationTiming, animationDelay,
    cssReset = {};

  • vendors: Maps CSS vendor prefixes (Webkit, Moz, O) to their corresponding JavaScript event prefixes.
  • testEl: A temporary DOM element used to detect which prefixed properties are supported.
  • supportedTransforms: A regular expression identifying valid transform functions such as translate3d(), rotateY(), etc.
  • cssReset: Stores reset values for animated properties to clean up styles after animation completes.

Vendor Prefix Detection

if (testEl.style.transform === undefined) {
  $.each(vendors, function(vendor, event) {
    if (testEl.style[vendor + 'TransitionProperty'] !== undefined) {
      prefix = '-' + vendor.toLowerCase() + '-';
      eventPrefix = event;
      return false; // exit loop once match found
    }
  });
}

This block checks whether native transform is supported. If not, it probes each vendor-prefixed version of TransitionProperty to determine the appropriate prefix. Once detected, prefix holds the CSS prefix (e.g., -webkit-), and eventPrefix stores the corresponding lowercase event namespace (e.g., webkit).

Initialize Prefixed Properties

transform = prefix + 'transform';

cssReset[transitionProperty = prefix + 'transition-property'] =
cssReset[transitionDuration = prefix + 'transition-duration'] =
cssReset[transitionDelay    = prefix + 'transition-delay'] =
cssReset[transitionTiming   = prefix + 'transition-timing-function'] =
cssReset[animationName      = prefix + 'animation-name'] =
cssReset[animationDuration  = prefix + 'animation-duration'] =
cssReset[animationDelay     = prefix + 'animation-delay'] =
cssReset[animationTiming    = prefix + 'animation-timing-function'] = '';

All relevant transition and animation CSS property are assigned prefixed names and initialized to empty strings within cssReset, allowing them to be cleared post-animation.

Animation Configuration: $.fx

$.fx = {
  off: (eventPrefix === undefined && testEl.style.transitionProperty === undefined),
  speeds: { _default: 400, fast: 200, slow: 600 },
  cssPrefix: prefix,
  transitionEnd: normalizeEvent('TransitionEnd'),
  animationEnd: normalizeEvent('AnimationEnd')
};

  • off: Boolean indicating lack of transition/animation support; disables animations when both standard and prefixed features are missing.
  • speeds: Predefined durations in milliseconds for named speed settings.
  • cssPrefix: The detected CSS vendor prefix string.
  • transitionEnd, animationEnd: Normalized event types for listening to animation lifecycle events.

Main Interface: $.fn.animate

$.fn.animate = function(properties, duration, ease, callback, delay) {
  if ($.isFunction(duration)) {
    callback = duration;
    ease = undefined;
    duration = undefined;
  }
  if ($.isFunction(ease)) {
    callback = ease;
    ease = undefined;
  }
  if ($.isPlainObject(duration)) {
    ease = duration.easing;
    callback = duration.complete;
    delay = duration.delay;
    duration = duration.duration;
  }

  if (duration) {
    duration = (typeof duration === 'number' ? duration :
               ($.fx.speeds[duration] || $.fx.speeds._default)) / 1000;
  }
  if (delay) {
    delay = parseFloat(delay) / 1000;
  }

  return this.anim(properties, duration, ease, callback, delay);
};

The animate method acts as a flexible wrapper around anim, handling various calling signatures:

  • .animate(props, callback)
  • .animate(props, duration, callback)
  • .animate(props, { duration, easing, complete, delay })

It normalizes arguments, converts millisecond durations to seconds (for CSS use), and maps symbolic durations like 'fast' to numeric values.

Core Animation Engine: $.fn.anim

$.fn.anim = function(properties, duration, ease, callback, delay) {
  var key, cssValues = {}, cssProperties = [], transforms = '',
      that = this, wrappedCallback, endEvent = $.fx.transitionEnd,
      fired = false;

  if (duration === undefined) duration = $.fx.speeds._default / 1000;
  if (delay === undefined) delay = 0;
  if ($.fx.off) duration = 0;

  if (typeof properties === 'string') {
    // Handle keyframe animation
    cssValues[animationName] = properties;
    cssValues[animationDuration] = duration + 's';
    cssValues[animationDelay] = delay + 's';
    cssValues[animationTiming] = ease || 'linear';
    endEvent = $.fx.animationEnd;
  } else {
    // Handle CSS transitions
    for (key in properties) {
      if (supportedTransforms.test(key)) {
        transforms += key + '(' + properties[key] + ') ';
      } else {
        cssValues[key] = properties[key];
        cssProperties.push(dasherize(key));
      }
    }

    if (transforms) {
      cssValues[transform] = transforms;
      cssProperties.push(transform);
    }

    if (duration > 0 && typeof properties === 'object') {
      cssValues[transitionProperty] = cssProperties.join(', ');
      cssValues[transitionDuration] = duration + 's';
      cssValues[transitionDelay] = delay + 's';
      cssValues[transitionTiming] = ease || 'linear';
    }
  }

  wrappedCallback = function(event) {
    if (event && event.target !== event.currentTarget) return;
    $(event ? event.target : this).unbind(endEvent, wrappedCallback);
    fired = true;
    $(this).css(cssReset);
    callback && callback.call(this);
  };

  if (duration > 0) {
    this.bind(endEvent, wrappedCallback);
    setTimeout(function() {
      if (fired) return;
      wrappedCallback.call(that);
    }, (duration + delay) * 1000 + 25);
  }

  // Trigger reflow to ensure animation starts
  this.size() && this.get(0).clientLeft;

  this.css(cssValues);

  if (duration <= 0) {
    setTimeout(function() {
      that.each(function() { wrappedCallback.call(this); });
    }, 0);
  }

  return this;
};

Parameter Initialization

If no duration is given, defaults to 400ms converted to seconds. Delay defaults to zero. When animations are unsupported ($.fx.off === true), duration is forced to 0, triggering immediate execution.

Handling Keyframe Animations

When properties is a string, it’s treated as an animation name from a @keyframes rule. Corresponding animation CSS rules are set with proper timing and delay. The end event changes to animationEnd.

Handling Transition-Based Animations

For object-based input:

  • Transform properties (e.g., rotate, scale) are concatenated into a single transform value.
  • Other style properties are added directly to cssValues and their dashed names pushed into cssProperties.
  • If transformations exist, they’re included in both the style map and property list.
  • If duration is positive, all transition-related CSS properties are configured.

Completion Callback Handling

The wrappedCallback ensures:

  • The event target matches currentTarget to prevent bubbling interference.
  • The listener is unbound after firing.
  • The fired flag prevents duplicate execution.
  • CSS properties involved in animation are reset via cssReset.
  • The user-provided callback is executed in the correct context.

Binding End Events and Fallback Timeout

An event listener is attached for transitionend or animationend. A fallback setTimeout triggers 25ms after expected completion time to handle cases where the event isn’t fired (common in older Android browsers). The fired guard ensures only one invocation occurs.

Triggering Reflow

this.size() && this.get(0).clientLeft;

Reading clientLeft forces a layout flush before applying new styles, ensuring the browser recognizes the change as a transitionable state. This trick guarantees that animations start correctly rather than jumping to final states.

Immediate Execution for Zero Duration

If duration ≤ 0, the callback is scheduled asynchronously using setTimeout(..., 0) to maintain consistent asynchronous behavior, mimicking real animation flow even when skipped.

Tags: zepto css3-transitions css3-animations vendor-prefixes front-end

Posted on Wed, 20 May 2026 17:15:56 +0000 by simanta