Skip to main
Table of Contents

Interactive Disabled Input

About

This demo shows a cross-browser pattern for giving users feedback when they click disabled form controls, including a shake animation and disabled cursor behavior. Internet Explorer is not included.

Demonstration

Try clicking or tapping the disabled fields below. You should see a not-allowed cursor over disabled targets on hover and a brief shake animation on click or touch.

Status: Ready

Disabled Inputs


Code

Javascript

disabled_shake.js

// events.js

function addEvent(evnt, elem, func) {
  if (elem.addEventListener) { // W3C DOM
    elem.addEventListener(evnt, func, false);
  }
  else if (elem.attachEvent) { // IE DOM
    elem.attachEvent('on' + evnt, func);
  }
  else { // No much to do
    elem['on' + evnt] = func;
  }
}

// classes.js

/**
 * Returns whether the given element has the given class.
 *
 * @param {Element} element
 * @param {string} className
 * @returns {boolean}
 */
function hasClass(element, className) {
	className = " " + className + " ";
	return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
}

function addClass(elements, className) {
  if ( elements.length === undefined ) elements = [elements];
  for (var i = 0; i < elements.length; i++) {
    var element = elements[i];
    if (!hasClass(element, className)) {
      if (element.classList) {
        element.classList.add(className);
      } else {
        element.className = ( element.className + ' ' + className ).replace( /\s+/g, ' ');
      }
    }
  }
}

function removeClass(elements, className) {
  if ( elements.length === undefined ) elements = [ elements ];
  for (var i = 0; i < elements.length; i++) {
    var element = elements[i];
    if (hasClass(element, className)) {
      if (element.classList) {
        element.classList.remove(className);
      } else {
        element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
      }
    }
  }
}

// throttle.js

var throttleGateKeys = {
  mousemove: false,
  click: false
};

var throttleGateTimes = {
  mousemove: 0,
  click: 0
};

var throttleGateDebounceEvents = {
  mousemove: false,
  scroll: false,
  click: false
};

var throttleGateIntervals = {
  mousemove: 100,
  click: 200 // 200ms is disabled animation duration
};

var throttleGateDebounceIntervalMin = 10;

function throttleGate(affectedKey, intervalKey) {
// function throttleGate(affectedKey, forceGate) {
  if (throttleGateKeys[intervalKey]) return true;
  // if (throttleGateKeys[affectedKey] || forceGate) return true;

  throttleGateKeys[affectedKey] = true;

  setTimeout(function() {
    throttleGateKeys[affectedKey] = false;
  }, throttleGateIntervals[affectedKey]);

  throttleGateTimes[affectedKey] = (new Date()).getTime(); // for debounce timing

  return false;
}

function debounceThrottleGate(key, debounceKey, func, event) {
  var intervalKey = key;

  if (key != 'click') {
    if(throttleGateDebounceEvents['click']) {
      // if within current click debounce time and inside last clicked rectangle.
      //    do not set interval key, instead call true in the throttle gate anyways (forceGate parameter)
      intervalKey = 'click';
    }
  }

  if (throttleGate(key, intervalKey)) {
    var throttleInterval = throttleGateIntervals[intervalKey];

    clearTimeout(throttleGateDebounceEvents[debounceKey]);

    throttleGateDebounceEvents[debounceKey] = setTimeout(function() {
      if (event) {
        func(event);
      }
      else {
        func();
      }
    }, Math.min(Math.max(throttleInterval - ((new Date()).getTime() - throttleGateTimes[intervalKey]), 0), throttleInterval) + throttleGateDebounceIntervalMin);

    return true;
  }

  return false;
}

// disabled_shake.js

var root = document.documentElement;

var lastMouseX = 0;
var lastMouseY = 0;

function updateMouseCoords(coordX, coordY) {
  lastMouseX = coordX;
  lastMouseY = coordY;
}

function isMouseCoordsInElem(elem) {
  var rect = elem.getBoundingClientRect();
  return parseInt(rect.left, 10) < lastMouseX && lastMouseX < parseInt(rect.left + rect.width, 10) && parseInt(rect.top, 10) < lastMouseY && lastMouseY < parseInt(rect.top + rect.height, 10);
}

var topLevelElemOverlapped;

function getTopLevelElemOverlapped(elem) {
  var isWithin = isMouseCoordsInElem(elem);

  if (isWithin) {
    topLevelElemOverlapped = elem;

    var hasDecendants = elem.hasChildNodes();

    if (hasDecendants) {
      var decendants = elem.children;
      var len = decendants.length;

      for (var i = 0; i < len; i++) {
        getTopLevelElemOverlapped(decendants[i]);
      }
    }
  }
}

function isElementDisabled(element) {
  return element && (element.disabled || hasClass(element, 'disabled'));
}

function tryDisabledClickRejection(elem) {
  if (isElementDisabled(elem)) {
    addClass(elem, 'show-disabled-animation');

    setTimeout(function() {
      removeClass(elem, 'show-disabled-animation');
    }, throttleGateIntervals['click']);
  }
}

function tryClick(event) {
  if(debounceThrottleGate('click', 'click', tryClick, event)) return;

  updateMouseCoords(event.x, event.y);
  getTopLevelElemOverlapped(root);

  if (topLevelElemOverlapped && isElementDisabled(topLevelElemOverlapped)) {
    tryDisabledClickRejection(topLevelElemOverlapped.parentNode);

    var elems = topLevelElemOverlapped.parentNode.children;
    var len = elems.length;

    for (var i = 0; i < len; i++) {
      var elem = elems[i];
      tryDisabledClickRejection(elem);
    }
  }
}

function tryDisabledCursor() {
  getTopLevelElemOverlapped(root);

  if( isElementDisabled(topLevelElemOverlapped) ) {
    addClass(root, 'show-disabled-cursor');
  }
  else {
    removeClass(root, 'show-disabled-cursor');
  }
}

function tryScroll() {
  if(debounceThrottleGate('mousemove', 'scroll', tryScroll, false)) return;

  tryDisabledCursor();
}

function tryMousemove(event) {
  if(debounceThrottleGate('mousemove', 'mousemove', tryMousemove, event)) return;

  updateMouseCoords(event.x, event.y);
  tryDisabledCursor();
}

var areDisabledElements = false;

function initDisabledShake() {
  if ( areDisabledElements ) return; // already running;

  var inputs = document.querySelectorAll('input, textarea');

  for ( var i = 0; i < inputs.length; i++ )
  {
    if ( inputs[i].disabled )
    {
      areDisabledElements = true;
      break;
    }
  }

  if ( areDisabledElements ) {
    addEvent('click', document, function(event) {
      // event.preventDefault(); // only if over disabled element || is click anim throttled?
      tryClick(event);
    });

    addEvent('scroll', document, function() {
      tryScroll();
    });

    addEvent('mousemove', document, function(event) {
      tryMousemove(event);
    });
  }
}

initDisabledShake();

CSS

disabled_shake.css

html.show-disabled-cursor {
  cursor: not-allowed;
}

@keyframes disabledAnimation {
  0% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-$page_gap);
  }
  75% {
    transform: translateX($page_gap);
  }
  100% {
    transform: translateX(0);
  }
}

.disabled, :not(.disabled) > :disabled {
  display: inline-block;
  pointer-events: none;
}

.show-disabled-animation {
  animation-duration: 0.2s;
  animation-iteration-count: 1;
  animation-name: disabledAnimation;
}

Attribution

This page is comprised of my own additions and either partially or heavily modified elements from the following source(s):