Go to homepage

Projects /

Expandable Project Presentation

A gallery of project preview images that expand on click to reveal the full case study.

Expandable Project Presentation
Check our new component library →

The real power of CSS Transitions is in allowing a smooth passage from point A to point B. The user is driven through the change, he is not presented with an immediate new result. It's these extra keyframes that make it possible to create pleasent motion-like web experiences.

In this example we take advantage of CSS Transitions and Transformations, and of the background-attachment CSS property to create a "diving-in" effect and reveal additional content for each project.

Image credits: Unsplash.com.

Creating the structure

The HTML structure is an unordered list wrapped inside a <div> element. Each list item contains a div.cd-title (title and brief description) and a div.cd-project-info (additional information). The project image is set as background image of the list item ::after pseudo-element.

<div class="projects-container">
         <div class="cd-title">
            <h2>Project 1</h2>
            <p>Brief description of the project here</p>
         </div> <!-- .cd-title -->

         <div class="cd-project-info">
            <p><!-- your content here --></p>
         </div> <!-- .cd-project-info -->

         <!-- .... -->

      <!-- .... -->
   <a href="#0" class="cd-close">Close</a>
   <a href="#0" class="cd-scroll">Scroll</a>
</div> <!-- .project-container -->

Adding style

On small devices, each list item has width equal to the viewport width, height equal to one-fourth of the viewport height (4 projects in our demo) and a translateX(-100%) so that it is moved outside the viewport. After the background-images are loaded (event detected in jQuery), the .is-loaded class is assigned to the list items (translateX(0)) to move them back in the viewport. CSS3 transitions has been used to achieve a smooth animation.

.projects-container li {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 25%;
  transition: transform 0.4s;
  /* on mobile -  move items outside the viewport */
  transform: translateX(-100%);
.projects-container li.is-loaded {
  /* move items in the viewport when background images have been loaded */
  transform: translateX(0);
.projects-container li::after {
  /* background image */
  background-image: url("../img/img-1-small.jpg");
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
.projects-container li:nth-of-type(2) {
  top: 25vh;
.projects-container li:nth-of-type(2)::after {
  background-image: url("../img/img-2-small.jpg");
/*other projects*/

When user clicks one project, the .is-full-width class is assigned to the selected list item: the ::after pseudo-element height is set to 100vh (viewport units), while the .cd-project-info visibility is changed to visible.

.projects-container li.is-full-width {
  /* selected item */
  top: 0;
  height: auto;
  z-index: 1;
.projects-container li.is-full-width::after {
  height: 100vh;

.cd-project-info {
  visibility: hidden;
  opacity: 0;
.is-full-width .cd-project-info {
  visibility: visible;
  opacity: 1;

On bigger screens, each list item has height equal to the viewport height and width equal to one-fourth of the viewport width. Besides, the background-attachment of the list items ::after pseudo-element has been set to fixed: this way the image is fixed with regard to the viewport (doesn't move while the selected project is scrolled) and covers the entire viewport (background-size: cover).

One note: we have been using the list-item ::before pseudo-element content property to access (in the js file) the background-image url attribute (this is used to detect if the background-images have been loaded). Therefore, each time you set a new background-image for the ::after pseudo-element, you need to update the content attribute of the ::before pseudo element as well.

.projects-container li::after {
  background-image: url("../img/img-1-small.jpg");
.projects-container li::before {
  /* never visible - this is used in jQuery to detect if the background image has been loaded  */
  content: 'img/img-1-small.jpg';
  display: none;
@media only screen and (min-width: 1024px) {
  .projects-container li:first-of-type::after {
    background-image: url("../img/img-1-large.jpg");
  .projects-container li:first-of-type::before {
    content: 'img/img-1-large.jpg';
/*other projects*/

Events handling

We used jQuery to detect when project background-images are loaded: as soon as they are, the loop function showCaption() is called to assign the .is-loaded class to each list item.
Besides, the click event on the .cd-close and list item elements is detected to expand/close a project.

Level up your CSS skills

Each month we email a 1-minute CSS tutorial to 20K developers

Awesome! We just sent you a confirmation link by email

Error - please try again or contact us

Your email address is already subscribed

Project duplicated

Project created

Globals imported

There was an error while trying to export your project. Please try again or contact us.