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.

Project duplicated

Project created

Globals imported

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