javascriptcss

CSS Expand / Contract Animation to Show/Hide Content


I am trying to create a box that can expand and collapse with a simple slide out animation. If you run the example below, the idea is that it starts with one red line and when you click the button it separates into two read lines and gently expands to reveal the content like pulling a draw out of a table.

I've tried both transform, animation, relative: positioning with top, and i'm unable to get the desired effect.

The containing box should expand in size

function expandContract() {
   const el = document.getElementById("expand-contract")
   el.classList.toggle('expanded')
   el.classList.toggle('collapsed')
}
#container {
   border: 1px solid black;
   padding: 15px;
}

#top-section {
  border-bottom: 1px solid red;
}

#expand-contract {
  border-bottom: 1px solid red;
}

.expand-contract {
   transform: translateY(-100%)
   overflow: hidden;
}

@keyframes slide-in {
    100% {
        transform: translateY(0%)
    }
}

.expanded {
   background-color: green;
   animation-name: slide-in;
   animation-duration: 1s;
}

.collapsed {
   background-color: red;
   transform: translateY(-100%)
}
<div id="container">
  <div id="top-section">
    This is always displayed
  </div>
  
  <div id="expand-contract" class="expanded">
    This section expands and contracts
  
    <table>
      <tr><td>test1</td></tr>
      <tr><td>test2</td></tr>
      <tr><td>test3</td></tr>
      <tr><td>test4</td></tr>
    </table>
  </div>
  
  <div id="bottom-section">
    This section is always displayed
  </div>
</div>

<button onclick="expandContract()">Expand/Contract</button>


Solution

  • You can achieve this using the CSS transition along with toggled styles. Initially you may think to transition the height (from 0 to initial so that it expands dynamically based on height) but unfortunately CSS transition doesn't properly handle this.

    Instead, you can wrap it in a container of its own with overflow: hidden and then use a margin-top: -100% to hide it, and 0 to show it.

    Here is your code with this modification:

    function expandContract() {
       const el = document.getElementById("expand-contract")
       el.classList.toggle('expanded')
       el.classList.toggle('collapsed')
    }
    #container {
       border: 1px solid black;
       padding: 15px;
    }
    
    #top-section {
      border-bottom: 1px solid red;
    }
    
    #expand-container {
      overflow: hidden;
    }
    
    #expand-contract {
      border-bottom: 1px solid red;
      margin-top: -100%;
      transition: all 1s;
    }
    
    #expand-contract.expanded {
       background-color: green;
       margin-top: 0;
    }
    <div id="container">
      <div id="top-section">
        This is always displayed
      </div>
      
      <div id="expand-container">
        <div id="expand-contract" class="expanded">
          This section expands and contracts
      
          <table>
            <tr><td>test1</td></tr>
            <tr><td>test2</td></tr>
            <tr><td>test3</td></tr>
            <tr><td>test4</td></tr>
          </table>
        </div>
      </div>
      
      <div id="bottom-section">
        This section is always displayed
      </div>
    </div>
    
    <button onclick="expandContract()">Expand/Contract</button>