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 astranslate3d(),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 singletransformvalue. - Other style properties are added directly to
cssValuesand their dashed names pushed intocssProperties. - 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
firedflag 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.