Expandable Cards on Click in Vanilla JavaScript

Expandable Cards on Click in Vanilla JavaScript
Project: Animated color cards
Author: Alex Zaworski
Edit Online: View on CodePen
License: MIT

This JavaScript code snippet helps you to create expandable cards on click. It is responsible for animating blocks on a webpage when they are clicked. It achieves this by calculating the necessary transforms (translation, rotation, and scaling) required to transition between the inactive and active states of the block and then applying these transforms to the block using CSS.

The code also adds and removes CSS classes to trigger the animations and handle the state changes of the blocks.

How to Create Expandable Cards on Click in Vanilla JavaScript

First of all, load the following assets into the head tag of your HTML document.

<script>
if (location.pathname.match(/fullcpgrid/i)) {
  document.documentElement.classList.add('grid-view');
  setTimeout(() => {
   document.querySelector('.block.y').classList.add('fake-hover');  
  }, 1750);
  setTimeout(() => {
   document.querySelector('.block.y').dispatchEvent(new Event('click'));  
  }, 2250);
}
</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">

Create the HTML structure for the expandable card as follows:

<div class="block-wrap">
  <div class="block-col">
    <div class="block block--transition r">
      <div class="block-content">
        <div class="block-content__header">
          <h2 class="block-content__header__text">Red</h2>
        </div>
        <p class="block-content__body">Red is the color at the end of the visible spectrum of light, next to orange and opposite violet. It has a dominant wavelength of approximately 625–740 nm.</p>
        <div class="block-content__button">
          Neato
        </div>
      </div>
    </div>
  </div>
  <div class="block-col">
    <div class="block block--transition  b">
      <div class="block-content">
        <div class="block-content__header">
          <h2 class="block-content__header__text">Blue</h2>
        </div>
        <p class="block-content__body">Blue is one of the three primary colours of pigments in painting and traditional colour theory, as well as in the RGB colour model. It lies between violet and green on the spectrum of visible light.</p>
        <div class="block-content__button">
          Very Cool
        </div>
      </div>
    </div>
  </div>
  <div class="block-col">
    <div class="block block--transition y">
      <div class="block-content">
        <div class="block-content__header">
          <h2 class="block-content__header__text">Yellow</h2>
        </div>
        <p class="block-content__body">Yellow is the color between orange and green on the spectrum of visible light. It is evoked by light with a dominant wavelength of roughly 570–590 nm. It is a primary color in subtractive color systems.</p>
        <div class="block-content__button">
          Yikes this one's low contrast
        </div>
      </div>
    </div>
  </div>
</div>

Now, style the expandable card using the following CSS styles:

*,
*:before,
*:after {
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: #e7e7e7;
  height: 100vh;
  display: flex;
}
.cd__main{
display: block !important;
}

.grid-view {
  transform: scale(0.75);
}

.block-wrap {
  width: 360px;
  height: 360px;
  background: #efefef;
  border-bottom: 2px solid #c9c9c9;
  box-shadow: inset 0 1px 0 #fcfcfc, 0 8px 8px -8px #c9c9c9, 0 12px 12px -8px #e2e2e2;
  display: flex;
  position: relative;
  overflow: hidden;
  justify-content: center;
  border-radius: 8px;
  margin: auto;
}

.block-col {
  display: flex;
  height: 100%;
  margin: 0 12px;
  width: 64px;
  flex-shrink: 0;
}

.block {
  height: 64px;
  width: 100%;
  border-radius: 8px;
  margin: auto 0;
  position: relative;
  will-change: transform;
}
.block:not(.block--active) {
  cursor: pointer;
}
.block:not(.block--active):hover, .block:not(.block--active).fake-hover {
  transform: translateY(-12px);
}
.block:not(.block--active):hover:after, .block:not(.block--active).fake-hover:after {
  content: "";
  display: block;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transform: scale(1.1) translateY(12px);
}
.block:not(.block--active):active {
  transform: scale(0.9) translateY(-12px);
}
.block--active {
  position: absolute;
  left: 0;
  z-index: 1;
  height: 100%;
  width: 100%;
}
.block--transition {
  transition: transform 0.185s cubic-bezier(0.4, 0, 0, 1);
}

.block-content {
  display: none;
  padding: 24px;
}
.block--active .block-content {
  display: block;
}

.block-content__header {
  background: rgba(0, 0, 0, 0.1);
  padding: 24px;
  margin: -24px -24px 24px;
  overflow: hidden;
}

.block-content__header__text {
  will-change: transform;
  margin: 0;
  opacity: 0;
  font-size: 2em;
}
.block--active .block-content__header__text {
  -webkit-animation: content-in 0.225s cubic-bezier(0, 0, 0.2, 1) forwards;
          animation: content-in 0.225s cubic-bezier(0, 0, 0.2, 1) forwards;
  -webkit-animation-delay: 0.15s;
          animation-delay: 0.15s;
}

.block-content__body {
  will-change: transform;
  opacity: 0;
  font-size: 18px;
  line-height: 1.333;
  -webkit-animation: content-in 0.245s cubic-bezier(0, 0, 0.2, 1) forwards;
          animation: content-in 0.245s cubic-bezier(0, 0, 0.2, 1) forwards;
  -webkit-animation-delay: 0.1s;
          animation-delay: 0.1s;
  margin: 0 0 20px;
}

.block-content__button {
  font-weight: bold;
  background: rgba(255, 255, 255, 0.25);
  display: inline-block;
  padding: 16px;
  border-radius: 4px;
  color: rgba(255, 255, 255, 0.85);
  -webkit-animation: button-in 0.245s cubic-bezier(0, 0, 0.2, 1) forwards;
          animation: button-in 0.245s cubic-bezier(0, 0, 0.2, 1) forwards;
  -webkit-animation-delay: 0.2s;
          animation-delay: 0.2s;
  opacity: 0;
  cursor: pointer;
}

@-webkit-keyframes content-in {
  0% {
    opacity: 0;
    transform: translateY(128px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes content-in {
  0% {
    opacity: 0;
    transform: translateY(128px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
@-webkit-keyframes button-in {
  0% {
    opacity: 0;
    transform: translateY(64px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes button-in {
  0% {
    opacity: 0;
    transform: translateY(64px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
.r {
  background: #f44336;
  color: #410804;
  border-top: 2px solid #f77066;
}
.r:not(.block--active) {
  border-bottom: 2px solid #ea1c0d;
  box-shadow: 0 8px 8px -8px #d2190b, 0 12px 12px -8px rgba(244, 67, 54, 0.4);
}
.r.block--active {
  background: #faf0ef;
}
.r .block-content__header {
  background: #f44336;
}
.r .block-content__button {
  background: #f44336;
  border-bottom: 2px solid #ea1c0d;
  border-top: 2px solid #f77066;
}

.b {
  background: #1e88e5;
  color: #03101a;
  border-top: 2px solid #4ca0ea;
}
.b:not(.block--active) {
  border-bottom: 2px solid #166dba;
  box-shadow: 0 8px 8px -8px #1360a4, 0 12px 12px -8px rgba(30, 136, 229, 0.4);
}
.b.block--active {
  background: #d5e2ed;
}
.b .block-content__header {
  background: #1e88e5;
}
.b .block-content__button {
  background: #1e88e5;
  border-bottom: 2px solid #166dba;
  border-top: 2px solid #4ca0ea;
}

.y {
  background: #fdd835;
  color: #4c3e01;
  border-top: 2px solid #fee268;
}
.y:not(.block--active) {
  border-bottom: 2px solid #fdce03;
  box-shadow: 0 8px 8px -8px #e3ba02, 0 12px 12px -8px rgba(253, 216, 53, 0.4);
}
.y.block--active {
  background: #fcfbf5;
}
.y .block-content__header {
  background: #fdd835;
}
.y .block-content__button {
  background: #fdd835;
  border-bottom: 2px solid #fdce03;
  border-top: 2px solid #fee268;
}

Finally, add the following JavaScript function for its effect:

const ACTIVE_CLASS = "block--active";
const TRANSITION_CLASS = "block--transition";

const getTransforms = (a, b) => {
  const scaleY = a.height / b.height;
  const scaleX = a.width / b.width;

  // dividing by 2 centers the transform since the origin
  // is centered not top left
  const translateX = a.left + a.width / 2 - (b.left + b.width / 2);
  const translateY = a.top + a.height / 2 - (b.top + b.height / 2);

  // nothing particularly clever here, just using the
  // translate amount to estimate a rotation direction/amount.
  // ends up feeling pretty natural to me.
  const rotate = translateX;

  return [
    `translateX(${translateX}px)`,
    `translateY(${translateY}px)`,
    `rotate(${rotate}deg)`,
    `scaleY(${scaleY})`,
    `scaleX(${scaleX})`
  ].join(" ");
};

const animate = (block, transforms, oldTransforms) => {
  block.style.transform = transforms;
  block.getBoundingClientRect(); // force redraw
  block.classList.add(TRANSITION_CLASS);
  block.style.transform = oldTransforms;
  block.addEventListener(
    "transitionend",
    () => {
      block.removeAttribute("style");
    },
    { once: true }
  );
};

[...document.querySelectorAll(".block")].forEach(block => {
  const buttonForBlock = block.querySelector(".block-content__button");
  block.addEventListener("click", event => {
    if (
      block.classList.contains(ACTIVE_CLASS) &&
      event.target !== buttonForBlock
    ) {
      return;
    }

    block.classList.remove(TRANSITION_CLASS);
    const inactiveRect = block.getBoundingClientRect();
    const oldTransforms = block.style.transform;

    block.classList.toggle(ACTIVE_CLASS);
    const activeRect = block.getBoundingClientRect();
    const transforms = getTransforms(inactiveRect, activeRect);

    animate(block, transforms, oldTransforms);
  });
});

That’s all! hopefully, you have successfully created expandable cards on click in vanilla JavaScript. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *