7

So I have this great little nugget of code that I am trying to rewrite into plain JavaScript and CSS without jQuery.

jQuery.extend(jQuery.easing,{
    easeInExpo: function (x, t, b, c, d) {
        return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
    }
}); 

var nset = false;
$('button#hmenu').click(function(){
    if(!nset){
        $('ul#nav').delay(35).slideDown(300,'easeInExpo');
        $('button#hmenu').addClass('active');
        nset = true;
    } else {
        $('ul#nav').slideUp(550);
        nset = false;
        $('button#hmenu').removeClass('active');
    }
})  

I'm looking at some CSS transitions that use easing but just wondering what the options are? In my code I have a slideDown and up easing functions. This is used in production for a mobile menu nav.

drooh
  • 578
  • 4
  • 18
  • 46
  • The options are easily found with Google: [CSS3 equivalent to jQuery slideUp and slideDown?](/q/5072526/4642212), [Need help changing from jquery to vanilla javascript](/q/68463309/4642212). Do you have some attempted CSS of your own? – Sebastian Simon Sep 30 '21 at 23:47
  • my question is specifically regarding the easeInExpo custom function, not just slideUp and down – drooh Sep 30 '21 at 23:50
  • Certainly you can use that question’s answers as a platform for your own experiments in solving the issue. Currently, you’re asking the volunteers who answer questions to reverse engineer jQuery animation for you, which seems to be asking a lot. – Heretic Monkey Oct 01 '21 at 00:15
  • Yep, that's what I'm asking for is help in which direction to go. If you don't want to help that is fine but there may be others who have dealt with an issue similar to mine. – drooh Oct 01 '21 at 00:19
  • @drooh I added a simplified example alongside my more complex example to show two ends of the spectrum. Because you may not always know the inner height of the content you're dealing with, we can use a little bit of JavaScript to retrieve and store the inner height of your expandable content areas, and then refresh those values any time the browser is resized. – Brandon McConnell Oct 05 '21 at 20:58

2 Answers2

9

Update:

This github repo (You don't need jQuery) has a really comprehensive list of common jQuery functions, all rewritten in vanilla Javascript.

It includes animations, query selectors, ajax, events and other advanced jQuery features.

The slightly older youmightnotneedjquery.com is also very good, particularly if you need to support older IE versions.

You can achieve an easeInExpo style animation by using the following CSS, as provided by this website:

transition: all 500ms cubic-bezier(0.950, 0.050, 0.795, 0.035);

The below example illustrates this easing property on the height of a div. I've updated it to match the delay you've added in jQuery when clicked (35ms), the timings (300ms and 550ms respectively), and jQuery's default easing ('swing') – as provided by this answer – when it's closed:

let expandable = document.getElementById('expandable');
let expandButton = document.getElementById('expand-button');

expandButton.addEventListener('click', () => {
  expandable.classList.toggle('expanded');
});
#expandable {
  background: red;
  transition: all 550ms cubic-bezier(.02, .01, .47, 1);
  height: 0px;
  width: 100px;
  transition-delay: 0ms;
}

#expandable.expanded {
  height: 100px;
  transition-delay: 35ms;
  transition: all 300ms cubic-bezier(0.950, 0.050, 0.795, 0.035);
}
<div id="expandable"></div>
<br />
<button id="expand-button">Toggle expand</button>
dave
  • 2,750
  • 1
  • 14
  • 22
  • This looks very nice, I’ll try to put together a more complete example, thx! – drooh Oct 01 '21 at 15:32
  • I've added a couple of links that should cover what you're looking for. – dave Oct 05 '21 at 03:01
  • Thanks, jQuery is good a making complex things clean. elegant and simple. It seems the vast majority of jQuery is obsolete but just a few of the more complex nuggets can be translated in various ways with CSS options and JavaScript options. – drooh Oct 05 '21 at 16:05
  • 1
    You might want to look at the [cash](https://github.com/fabiospampinato/cash) library if you only need to support (IE11+) newer browsers. It has many of jQuery's features (and shares its syntax), but is only about 10% the size of jQuery. – dave Oct 06 '21 at 03:50
  • 1
    To add to this – if you want to go the CSS route for animations [Animate.css](https://github.com/animate-css/animate.css) makes life a lot easier. And [velocity](https://github.com/julianshapiro/velocity) uses the same API as jQuery's `animate` but is generally more performant. – dave Oct 06 '21 at 04:03
  • wow Cash and Animate.css look very nice! – drooh Oct 06 '21 at 18:10
  • The only thing this is missing is the 35ms delay before enacting the animation, transition-delay: 35ms; – drooh Oct 09 '21 at 14:24
  • I've added `transition-delay` into the example for completeness. If you wanted to achieve the delay with Javascript you could use [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) inside your click event listener as well. – dave Oct 09 '21 at 23:16
  • I also added in the timings you've got on your jQuery example and the default 'swing' easing when it's closed, so it should match quite closely now. – dave Oct 10 '21 at 07:31
2

There's a little bit of magic needed here in order to get your collapsible component to expand to a variable height. If you are just expanding to the same height each time, say 100px, that's a simple as creating a single class such as "expanded" and then adding and removing that class as a boolean switch.

We'll still be using a boolean switch for variable heights, but we'll also need to get the heights of each expandable element using JavaScript, and then refresh those height values if the size of the user's window changes to account for text-wrapping, image resizing, etc.

We can achieve rather simply using custom CSS properties (variables), with a fallback value to unset, meaning that when to variable height is present, the box will expand to show all contents without an animation as a last resort, but in most if not all cases (and for all modern browsers) the custom CSS variable should be the ideal solution for unique values per section.

Here it is in action (below), with both a toggle and accordion example, with a cubic bezier easing function used to closely match the easeInExpo function you supplied in your original question. I pulled this easing function cubic-bezier(0.95, 0.05, 0.795, 0.035) from easings.net/en#easeInExpo where they have a pure CSS easing for easeInExpo and many others.

Simple Example

const expandables = document.querySelectorAll('.expandable');
const setInnerHeights = () => {
  for (const expandable of expandables) {
    expandable.style.setProperty('--inner-height', Array.from(expandable.children).map(child => child.offsetHeight).reduce((a, c) => a + c, 0) + 'px');
  }
};
setInnerHeights();
document.addEventListener('click', e => {
  if (e.target?.matches('.expand-trigger')) {
    const expandable = e.target.nextElementSibling;
    expandable.classList[expandable.classList.contains('expanded') ? 'remove' : 'add']('expanded');
  }
});
window.addEventListener('resize', setInnerHeights);
html {
  height: 100%;
  box-sizing: border-box;
}
*, *::before, *::after {
  box-sizing: inherit;
}
body {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  min-height: 100%;
  padding: 20px;
}
.expandable {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.25s cubic-bezier(0.95, 0.05, 0.795, 0.035);
  text-align: left;
}
.expandable > p {
  margin: 0;
  padding: 10px 0;
}
.expandable.expanded {
  --content-height: calc(var(--inner-height) + 20px);
  max-height: var(--content-height, unset);
}
<button class="expand-trigger">Expand #1</button>
<div class="expandable">
  <p>Lorem ipsum dolor sit amet.</p>
</div>
<button class="expand-trigger">Expand #2</button>
<div class="expandable">
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mi purus, interdum id mattis et, posuere nec urna. Mauris in ornare sem. Phasellus non eros augue. Fusce tempus bibendum mauris, vel luctus est viverra eget. Cras vitae lectus magna. Integer vulputate est ut felis dictum consectetur. Nunc vitae enim at sem rhoncus aliquet et id risus. Etiam faucibus quis turpis eu pellentesque. Aliquam dictum lorem nec orci finibus commodo. Etiam tincidunt lacinia consectetur. Praesent tortor lorem, imperdiet sed varius vel, varius ac quam. Vestibulum aliquam lorem sem, sit amet imperdiet purus commodo eu. Integer a iaculis tortor.</p>
</div>
<button class="expand-trigger">Expand #3</button>
<div class="expandable">
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mi purus, interdum id mattis et, posuere nec urna. Mauris in ornare sem. Phasellus non eros augue. Fusce tempus bibendum mauris, vel luctus est viverra eget. Cras vitae lectus magna. Integer vulputate est ut felis dictum consectetur. Nunc vitae enim at sem rhoncus aliquet et id risus. Etiam faucibus quis turpis eu pellentesque. Aliquam dictum lorem nec orci finibus commodo. Etiam tincidunt lacinia consectetur. Praesent tortor lorem, imperdiet sed varius vel, varius ac quam. Vestibulum aliquam lorem sem, sit amet imperdiet purus commodo eu. Integer a iaculis tortor.</p>
  <p>Integer convallis lectus eu felis bibendum, vel lacinia metus imperdiet. Maecenas vulputate, quam vitae tempus pretium, erat felis euismod risus, nec blandit leo mi eget purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer at neque laoreet, egestas dui ut, bibendum lorem. Maecenas elementum odio a congue facilisis. Vivamus risus urna, vestibulum egestas sem nec, lacinia volutpat metus. Suspendisse potenti. Suspendisse ullamcorper commodo libero, sed rhoncus nibh porta in. Donec mi felis, posuere luctus varius ac, faucibus vitae erat.</p>
</div>

Complex Example

const initAccordions = () => {
  const getNode = selector => document.querySelector(selector),
      getNodes = selector => Array.from(document.querySelectorAll(selector)),
      findChildren = (node, selector) => Array.from(node.children).filter(e => e.matches?.(selector)),
      findChild = (node, selector) => Array.from(node.children).find(e => e.matches?.(selector)),
      _addInput = (node, position, id, checked) => node.insertAdjacentHTML(position, `<input type="radio" name="accordion-${id}"${checked ? ' checked="checked"' : ''}>`),
      setInnerHeight = node => {
        const height = Array.from(node.children).map(child => child.offsetHeight).reduce((a, c) => a + c, 0) + 'px';
        node.style.setProperty('--inner-height', height);
      },
      accordions = Array.from(document.querySelectorAll('.accordion'));
  let accordionIndex = 0;
  for (const accordion of accordions) {
    const isToggle = accordion.dataset?.type === 'toggle',
        panels = findChildren(accordion, '.accordion--panel');
    let panelIndex = 0;
    for (const panel of panels) {
      const title = findChild(panel, '.accordion--panel--title'),
          content = findChild(panel, '.accordion--panel--content'),
          addInput = (node, position, checked) => _addInput(node, position, accordionIndex + (isToggle ? '-' + panelIndex : ''), checked);
      setInnerHeight(content);
      addInput(title, 'beforebegin');
      addInput(title, 'afterbegin', true);
      panelIndex++;
    }
    accordionIndex++;
  }
  window.addEventListener('resize', () => {
    const panelContents = Array.from(document.querySelectorAll('.accordion > .accordion--panel > .accordion--panel--content'));
    for (const content of panelContents) setInnerHeight(content);
  });
};
initAccordions();
html {
  height: 100%;
  box-sizing: border-box;
}
*, *::before, *::after {
  box-sizing: inherit;
}
body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  min-height: 100%;
  padding: 20px;
}
.accordion--panel > [type=checkbox],
.accordion--panel > [type=radio], .accordion--panel--title > [type=checkbox],
.accordion--panel--title > [type=radio] {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}
.accordion--panel > [type=checkbox]:checked,
.accordion--panel > [type=radio]:checked, .accordion--panel--title > [type=checkbox]:checked,
.accordion--panel--title > [type=radio]:checked {
  display: none;
}
.accordion--panel {
  border-radius: 7px;
}
.accordion--panel--title {
  background-color: #ccc;
}
.accordion--panel--content {
  box-shadow: inset 0 0 0 2px #ccc;
  border-radius: 0 0 7px 7px;
}
.accordion {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  max-width: 500px;
}
.accordion--panel {
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
}
.accordion--panel > [type=checkbox],
.accordion--panel > [type=radio] {
  z-index: 1;
}
.accordion--panel--title, .accordion--panel--content {
  padding-inline: 15px;
}
.accordion--panel--title {
  position: relative;
  padding-block: 10px;
}
.accordion--panel--content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.25s cubic-bezier(0.95, 0.05, 0.795, 0.035);
}
.accordion--panel--content--inner > p:first-child {
  margin-top: 10px;
}
.accordion--panel--content--inner > p:last-child {
  margin-bottom: 10px;
}
[type=checkbox]:checked ~ .accordion--panel--content, [type=radio]:checked ~ .accordion--panel--content {
  --content-height: calc(var(--inner-height) + 20px);
  max-height: var(--content-height, unset);
}
<h2>Accordion Demo</h2>
<div class="accordion" data-type="accordion">
  <div class="accordion--panel">
    <div class="accordion--panel--title">Title #1</div>
    <div class="accordion--panel--content">
      <div class="accordion--panel--content--inner">
        <p>Lorem ipsum dolor sit amet.</p>
      </div>
    </div>
  </div>
  <div class="accordion--panel">
    <div class="accordion--panel--title">Title #2</div>
    <div class="accordion--panel--content">
      <div class="accordion--panel--content--inner">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mi purus, interdum id mattis et, posuere nec urna. Mauris in ornare sem. Phasellus non eros augue. Fusce tempus bibendum mauris, vel luctus est viverra eget. Cras vitae lectus magna. Integer vulputate est ut felis dictum consectetur. Nunc vitae enim at sem rhoncus aliquet et id risus. Etiam faucibus quis turpis eu pellentesque. Aliquam dictum lorem nec orci finibus commodo. Etiam tincidunt lacinia consectetur. Praesent tortor lorem, imperdiet sed varius vel, varius ac quam. Vestibulum aliquam lorem sem, sit amet imperdiet purus commodo eu. Integer a iaculis tortor.</p>
      </div>
    </div>
  </div>
  <div class="accordion--panel">
    <div class="accordion--panel--title">Title #3</div>
    <div class="accordion--panel--content">
      <div class="accordion--panel--content--inner">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mi purus, interdum id mattis et, posuere nec urna. Mauris in ornare sem. Phasellus non eros augue. Fusce tempus bibendum mauris, vel luctus est viverra eget. Cras vitae lectus magna. Integer vulputate est ut felis dictum consectetur. Nunc vitae enim at sem rhoncus aliquet et id risus. Etiam faucibus quis turpis eu pellentesque. Aliquam dictum lorem nec orci finibus commodo. Etiam tincidunt lacinia consectetur. Praesent tortor lorem, imperdiet sed varius vel, varius ac quam. Vestibulum aliquam lorem sem, sit amet imperdiet purus commodo eu. Integer a iaculis tortor.</p>
        <p>Integer convallis lectus eu felis bibendum, vel lacinia metus imperdiet. Maecenas vulputate, quam vitae tempus pretium, erat felis euismod risus, nec blandit leo mi eget purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer at neque laoreet, egestas dui ut, bibendum lorem. Maecenas elementum odio a congue facilisis. Vivamus risus urna, vestibulum egestas sem nec, lacinia volutpat metus. Suspendisse potenti. Suspendisse ullamcorper commodo libero, sed rhoncus nibh porta in. Donec mi felis, posuere luctus varius ac, faucibus vitae erat.</p>
      </div>
    </div>
  </div>
</div>
<h2>Toggle Demo</h2>
<div class="accordion" data-type="toggle">
  <div class="accordion--panel">
    <div class="accordion--panel--title">Title #1</div>
    <div class="accordion--panel--content">
      <div class="accordion--panel--content--inner">
        <p>Lorem ipsum dolor sit amet.</p>
      </div>
    </div>
  </div>
  <div class="accordion--panel">
    <div class="accordion--panel--title">Title #2</div>
    <div class="accordion--panel--content">
      <div class="accordion--panel--content--inner">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mi purus, interdum id mattis et, posuere nec urna. Mauris in ornare sem. Phasellus non eros augue. Fusce tempus bibendum mauris, vel luctus est viverra eget. Cras vitae lectus magna. Integer vulputate est ut felis dictum consectetur. Nunc vitae enim at sem rhoncus aliquet et id risus. Etiam faucibus quis turpis eu pellentesque. Aliquam dictum lorem nec orci finibus commodo. Etiam tincidunt lacinia consectetur. Praesent tortor lorem, imperdiet sed varius vel, varius ac quam. Vestibulum aliquam lorem sem, sit amet imperdiet purus commodo eu. Integer a iaculis tortor.</p>
      </div>
    </div>
  </div>
  <div class="accordion--panel">
    <div class="accordion--panel--title">Title #3</div>
    <div class="accordion--panel--content">
      <div class="accordion--panel--content--inner">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mi purus, interdum id mattis et, posuere nec urna. Mauris in ornare sem. Phasellus non eros augue. Fusce tempus bibendum mauris, vel luctus est viverra eget. Cras vitae lectus magna. Integer vulputate est ut felis dictum consectetur. Nunc vitae enim at sem rhoncus aliquet et id risus. Etiam faucibus quis turpis eu pellentesque. Aliquam dictum lorem nec orci finibus commodo. Etiam tincidunt lacinia consectetur. Praesent tortor lorem, imperdiet sed varius vel, varius ac quam. Vestibulum aliquam lorem sem, sit amet imperdiet purus commodo eu. Integer a iaculis tortor.</p>
        <p>Integer convallis lectus eu felis bibendum, vel lacinia metus imperdiet. Maecenas vulputate, quam vitae tempus pretium, erat felis euismod risus, nec blandit leo mi eget purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer at neque laoreet, egestas dui ut, bibendum lorem. Maecenas elementum odio a congue facilisis. Vivamus risus urna, vestibulum egestas sem nec, lacinia volutpat metus. Suspendisse potenti. Suspendisse ullamcorper commodo libero, sed rhoncus nibh porta in. Donec mi felis, posuere luctus varius ac, faucibus vitae erat.</p>
      </div>
    </div>
  </div>
</div>

Other CSS easing functions are available at easings.net.

You can also customize your easing function visually using Chrome's DevTools by adding either the transition or transition-timing-function properties to any element with the value of ease, then clicking the square curve icon next to the word ease and dragging either of the circular handles.

Brandon McConnell
  • 5,776
  • 1
  • 20
  • 36