Odo Helpers

All the little things.


IE9+ with polyfill.

To support IE<=11, you will need to add classList polyfill.


Odo Device, Object.assign, String.prototype.includes

Animation Helpers

onTransitionEnd and cancelTransitionEnd


When the button is clicked, this example cancels the last transition end listener. It then updates the text and waits for a new transition end callback.

var transitionEndCount = 0;
var transitionId = null;
var $block = $('#animate-my-left');
var $output = $('#left-output');

$('#left-animator').on('click', function() {
  $output.text('waiting for transition: ' + transitionEndCount);

  transitionId = OdoHelpers.onTransitionEnd($block, function() {
    $output.text('transition ended: ' + transitionEndCount++);

Method Signature

This method can take an optional context to call the callback function with, a specific property to wait for the transition event on, and an optional timeout (in milliseconds) before the transition is considered complete and the callback will be invoked.

elem can be either a regular Element or jQuery instance.

var transitionId = OdoHelpers.onTransitionEnd(elem, fn, context, optProperty, optTimeout);

onAnimationEnd(elem, fn, context)

For keyframe animations that do not infinitely loop, you can get a callback when it finishes. In this example, the spinning class is added to the element which adds an animation-name property.

Side note: this is how you can restart a keyframe OdoHelpers.

Styles (prefixes omitted)

.my-keyframes {
  animation-duration: 500ms;
  animation-timing-function: ease-out;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;

.my-keyframes.spinning {
  animation-name: spin;

@keyframes spin {
  50% {
    border-radius: 50%;
    background-color: #27ae60;
  to {
    transform: rotate(360deg);
$('#spin-it').on('click', function() {
  var $block = $('.my-keyframes');
  $block.addClass('spinning spun');
  OdoHelpers.onAnimationEnd($block, function() {
  }, this);

Fading elements

fadeOutElement(elem, optFn, optThis, optInvisible)

fadeInElement(elem, optFn, optThis, optInvisible)

Fade an element in or out using classes. elem is the element to fade. You can also provide a callback for the second parameter, a context, and whether or not to add visibility:hidden|visible when the transition finishes.

Warning! This requires you to have 3 css classes. fade, in, and invisible.


Using all the parameters:

OdoHelpers.fadeOutElement(document.getElementById('darth-fader'), function() {
  console.log('Faded out.', this.hello);
}, { hello: 'world' }, true);

Remove the transition listener

The fade methods use onTransitionEnd internally and return the the same value onTransitionEnd does.

var fadeId = OdoHelpers.fadeInElement(document.getElementById('darth-fader'));

Required CSS

.fade {
  transition: opacity 150ms ease-out;
  opacity: 0;

.fade.in {
  opacity: 1;

.invisible {
  visibility: hidden;

scrollTo(position, speed, callback, easing) and scrollToTop()

With scrollTo and scrollToTop, you can animate the scroll top of the window.

$('#started-from-the-bottom').on('click', function() {
  var bottom = $(document).height() - $(window).height();
  var duration = 600;
  var easing = function (k) {
    return -k * (k - 2);
  OdoHelpers.scrollTo(bottom, duration, function() {
  }, easing);

$('#take-me-to-the-top').on('click', function() {

Animation Stepper

A requestAnimationFrame throttled replacement for jQuery's animate. It is a simple class for providing a stepping function for each animation frame over a given length of time. Its API is similar to the AnimationPlayer in Element.animate.

var stepper;
$('#step-it').on('click', function() {
  // Stop any previous steppers from finishing.
  if (stepper) {

  stepper = new OdoHelpers.Stepper({
    start: 0,
    end: 180,
    duration: 800,
    context: { taco: 'Tuesday' },
    step: function(value, percent) {
      // this === { taco: 'Tuesday' }
      // Position block
      // Update display.
      $stepOutput.html(value.toFixed(0) + '/180<br>' + Math.round(percent * 100) + '%')
    // Custom easing
    easing: function(k) {
      return -(--k * k * k * k - 1);

  stepper.onfinish = function() {
    $stepOutput.html($stepOutput.html() + '<br>Done!');

More easings

Array Helpers

closest(arr, num)

Given an array of numbers, find the item in the array closest to a given number.

OdoHelpers.closest([0, 100, 200, 300, 400, 500], 180) // 200

closestLessThan(arr, num)

Given an array of numbers, find the item in the array closest to a given number while also less than the given number.

OdoHelpers.closestLessThan([0, 100, 200, 300, 400, 500], 180) // 100

closestGreaterThan(arr, num)

Given an array of numbers, find the item in the array closest to a given number while also greater than the given number.

OdoHelpers.closestGreaterThan([0, 100, 200, 300, 400, 500], 200) // 300

chunk(array, size)

Make an array of smaller arrays from an array.

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
OdoHelpers.chunk(arr, 4) // [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]
OdoHelpers.chunk(arr, 5) // [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]

pull(array, item)

Remove an item from an array.

var letters = ['a', 'b', 'c', 'd'];
OdoHelpers.pull(letters, 'c');
console.log(letters); // ['a', 'b', 'd'];

Browser Helpers

Setting Hash

Sets the window location hash. This will create an entry in the user's history.

$('#hash-click').on('click', function() {

Setting History State

Sets the URL's hash like setHash, but this method does not create an entry in the user's history (unless the browser doesn't support replaceState (IE9)).

$('#replace-hash-click').on('click', function() {

User agent sniffing 🙃

Because sometimes you can't feature-test things.



Class for representing coordinates and positions.

Coordinate(optX, optY)

If the x or y value is not specified, it will be zero.

var Coordinate = OdoHelpers.Coordinate;
var start = new Coordinate(10, 50);
console.log(start.x); // 10
console.log(start.y); // 50

distance(a, b)

Calculate the distance between two coordinates.

var end = new Coordinate(50, 100);
Coordinate.distance(start, end); // -64.03

equals(a, b)

Determine whether two coordinates are the same.

var p1 = new Coordinate(10, 10);
var p2 = new Coordinate(10, 10);
var p3 = new Coordinate(21, 10);
Coordinate.equals(p1, p1); // true
Coordinate.equals(p1, p3); // false

sum(a, b)

Calculate the sum between two coordinates as a new Coordinate.

Coordinate.sum(start, end); // {x: 60, y: 150 }

difference(a, b)

Calculate the difference between two coordinates as a new Coordinate.

Coordinate.difference(start, end); // {x: -40, y: -50 }

product(a, b)

Calculate the product between two coordinates as a new Coordinate.

Coordinate.product(start, end); // {x: 500, y: 5000 }

quotient(a, b)

Calculate the quotient between two coordinates as a new Coordinate.

Coordinate.quotient(start, end); // {x: 0.2, y: 0.5 }

scale(a, sx, optSy)

Returns a scaled clone of a without modifying a.

Coordinate.scale(start, 0.5); // {x: 5, y: 25 }


Returns a new coordinate with this coordinates points.

var position = new Coordinate(100, 100);
var p2 = position.clone();
console.log(p2); // { x: 100, y: 100 }

scale(sx, optSy)

Scales this coordinate by the given scale factors.

var position = new Coordinate(100, 100);
console.log(position); // { x: 60, y: 60 }

translate(tx, ty)

Translates this box by the given offsets. If a Coordinate is given, then the x and y values are translated by the coordinate's x and y. Otherwise, x and y are translated by tx and opt_ty respectively.

var position = new Coordinate(100, 100);
position.translate(5, 10);
console.log(position); // { x: 105, y: 110 }
position.translate(new Coordinate(-5, -10));
console.log(position); // { x: 100, y: 100 }

DOM Helpers

There are more than what’s listed here. The unlisted ones are self-explanatory.

getRelativeDepth(node, parentNode)

Returns the depth of the given node relative to the given parent node, or -1 if the given node is not a descendant of the given parent node. E.g. if node == parentNode it returns 0, if node.parentNode == parentNode it returns 1, etc.

    I'm a text node.
var bob = document.querySelector('bob');
var grandpa = document.querySelector('grandpa');
OdoHelpers.getRelativeDepth(bob, grandpa); // 1

getNthSibling(node, n, optIsForward)

Retrieves the nth sibling of an element, or null if the would be nth sibling does not exist. Heads up! This function excludes text nodes.

  <slide data-start></slide>
  <slide data-find-me></slide>
var start = document.querySelector('slide[data-start]');
OdoHelpers.getNthSibling(start, 3); // <slide data-find-me></slide>
OdoHelpers.getNthSibling(start, 4); // null

DOM Ready Promise

Returns a promise that resolves once the document ready state is 'interactive'. If the DOMContentLoaded event has not been dispatched yet, the promise will resolve after this event is fired.

OdoHelpers.domReady resolved

OdoHelpers.domReady.then(function() {
  var end = new Date().getTime() - date;
  $('#dom-ready-text').text(end + ' ms');

DOM Loaded Promise

Returns a promise that resolves once the document ready state is 'complete'. If the load event has not been dispatched yet, the promise will resolve after this event is fired.

OdoHelpers.domLoaded resolved

OdoHelpers.domLoaded.then(function() {
  var end = new Date().getTime() - date;
  $('#dom-loaded-text').text(end + ' ms');

Math Helpers

Math utilities

clamp(value, min, max)

Takes a number and clamps it to within the provided bounds.

OdoHelpers.clamp(100, 0, 10) // 10
OdoHelpers.clamp(5, 0, 10) // 5
OdoHelpers.clamp(-1, 0, 10) // 0

wrapAroundList(index, displacement, length)

Calculates the offset index for a circular list. For example, a looping carousel. When you're at the last slide and increment the slide to wish to go to, it should go to the first one.

var currentIndex = 3;
var listLength = 4;
OdoHelpers.wrapAroundList(currentIndex, 1, listLength); // 0
OdoHelpers.wrapAroundList(currentIndex, 2, listLength); // 1
OdoHelpers.wrapAroundList(currentIndex, 3, listLength); // 2
OdoHelpers.wrapAroundList(currentIndex, 4, listLength); // 3
OdoHelpers.wrapAroundList(currentIndex, 5, listLength); // 0


Class for representing a box. A box is specified as a top, right, bottom, and left. A box is useful for representing margins and padding.

var marginBox = new OdoHelpers.Box(top, right, bottom, left);


Class for representing rectangular regions.

var boundary = new OdoHelpers.Rect(left, top, width, height);

String Helpers

Get gooder with words #yolo.


Be proper!

OdoHelpers.capitalize('foo'); // 'Foo'
OdoHelpers.capitalize('foo bar'); // 'Foo bar'


Hyphenates a javascript style string to a css one.

OdoHelpers.hyphenate('MozBoxSizing'); // '-moz-box-sizing.'
OdoHelpers.hyphenate('boxShadow'); // 'box-shadow'


Creates a random string for IDs, etc.

OdoHelpers.randomString(); // 'qw7i22r19k9'
OdoHelpers.randomString(); // '75042h5b3xr'

Style Helpers

Size, Padding, and Margins

Get the height and width of an element when the display is not none with getSize. If it or its parent is display none, this won't work properly.

I have margins and padding.
<div id="get-me" style="margin:5px auto 1em 2%;padding:1em 2% 5px 0;background:tan;">I have margins and padding.</div>
var element = document.getElementById('get-me');
var output = document.getElementById('its-raining');

var echo = function(str) {
  output.innerHTML = JSON.stringify(str, null, ' ');

$('#get-margins').on('click', function() {

$('#get-paddings').on('click', function() {

$('#get-size').on('click', function() {

getElementsSize(elements, dimension)

Returns the size (width or height) of a list of elements, including margins. It takes an array (not a NodeList) of elements and a dimension.

var wrapper = document.getElementById('get-elements-wrapper');
var output = document.getElementById('output-elements-size');

$('#get-elements-size').on('click', function() {
  var width = OdoHelpers.getElementsSize(Array.from(wrapper.children), 'width');
  output.textContent = width + ' pixels';


Force the page to be repainted in WebKit and Blink. This is often useful for native Android browsers because they do not update their state often enough, for example, after checking a checkbox.

In this example, you won't see anything change.

$('#force-redraw').on('click', OdoHelpers.forceRedraw);


Ask the browser for a property that will cause it to recalculate styles and layout the element (and possibly surrounding/parent elements).

In this example, the block is resting at 1 opacity. We want it to fade up from 0 to 1 opacity. By asking for the offsetWidth of the block (which is what causeLayout does), it forces the browser to apply any operations in the previous batch. Without the layout, the browser will batch-process your code and it will only see the block is supposed to be 1 opacity and it's already at 1 opacity, so there won't be a transition.

#blocktacular.faded {
  opacity: 0;

#blocktacular.active {
  opacity: 1;
  transition: 400ms;
var checkbox = document.getElementById('should-i-do-it');
var block = document.getElementById('blocktacular');

$('#lets-get-ready-to-rumble').on('click', function() {


  if (checkbox.checked) {


  setTimeout(function() {
    $(block).removeClass('faded active');
  }, 500);


Can we base-align the descriptions?

Use evenHeights to find the tallest element then set all the elements to that height.

SFO - Odo Hotspots

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Odo Scroll Animation

Ut enim ad minim veniam.

SFO - Odo The Longest Component of All Time!

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

Odo Carousel

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

$('#even-the-heights').on('click', function() {
  var tallest = OdoHelpers.evenHeights($('#first-row .product__title').get());
  console.log(tallest); // 34 ish

Aww, now the column backgrounds are still not aligned.

Multiple groups of elements can also be set. In this example, the second rows' .product and .product__title are set to the same height. This is much more performant than calling evenHeights twice.

Odo Carousel

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

SFO - Odo The Longest Component of All Time!

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

Odo Scroll Animation

Ut enim ad minim veniam.

SFO - Odo Hotspots

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

$('#even-them-all').on('click', function() {
  var groups = [
    $('#second-row .product__title').get(),
    $('#second-row .product').get()
  var tallestArray = OdoHelpers.evenHeights(groups);
  console.log(tallestArray); // [34, 140] ish

Timer Class

A simple timer class. The timer does not start automatically when initialized. OdoHelpers.Timer

continuous Option

If true, the timer will automatically restart itself when it expires.

Timer running.
var continuous = false;
var duration = 400;
var timer = new Timer(function() {
  $('#timer-block').css('backgroundColor', 'hsl(' + Math.round(Math.random() * 360) + ',58%,50%)');
}, duration, continuous);

General Utilities

defaultsTo(value, defaultValue, test)

Fall back to a specified default if an input is undefined or null. Optionally provide your own test for the value to pass.

OdoHelpers.defaultsTo('foo', 'bar') // 'foo'
OdoHelpers.defaultsTo(5, {}) // 5
OdoHelpers.defaultsTo(null, 'bar') // 'bar'
var fn = function() {};
OdoHelpers.defaultsTo(fn, 'bar', typeof fn === 'function') // fn
OdoHelpers.defaultsTo(fn, 'bar', typeof fn === 'number') // 'bar'


OdoHelpers.isString(5); // false
OdoHelpers.isString('JavaScript'); // true


OdoHelpers.isDefined(5); // true
OdoHelpers.isDefined(null); // false
OdoHelpers.isDefined(undefined); // false

getNumberOption(value, defaultValue)

Parse a value as a number. If it's not numeric, then the default value will be returned.

OdoHelpers.getNumberOption(5.2, 'foo'); // 5.2
OdoHelpers.getNumberOption('5.2', 'foo'); // 5.2
OdoHelpers.getNumberOption({}, 10); // 10

getStringOption(value, defaultValue)

Parse a value as a string. If it's not a string, then the default value will be returned.

OdoHelpers.getStringOption(5.2, 'foo'); // 'foo'
OdoHelpers.getStringOption('bar', 'foo'); // 'bar'

getPercentageOption(value, defaultValue)

Parse a value as a percentage. If it's a string with '%' in it, it will be parsed as a string, otherwise it will be parsed as a number.

OdoHelpers.getPercentageOption('50%', '100%'); // '50%'
OdoHelpers.getPercentageOption(50, '100%'); // 50
OdoHelpers.getPercentageOption(null, '100%'); // '100%'


A function which does nothing.



Need a cross-browser transitionend event name or the correct pointerdown string?

console.log('transition end event name: ' + OdoHelpers.events.TRANSITIONEND);
console.log('animation end event name: ' + OdoHelpers.events.ANIMATIONEND);
console.log('pointer move event name: ' + OdoHelpers.events.POINTERMOVE);