Creating a modern modal with CSS

Using CSS3 tech­niques a modal box can be cre­ated with­out JavaScript or images. With a bit of ani­ma­tion, tran­si­tion and trans­form, it can be made that lit­tle bit more special.

CSS Modal Experiment

Modal exper­i­ment updated for Fire­fox 10 which has bet­ter trans­form, tran­si­tion and ani­ma­tion per­for­mance. Also sup­ports 3D transforms.

In this exper­i­ment, click­ing an ‘open’ link pops up a dia­logue with a smooth hard­ware accel­er­ated bounce (where sup­ported). When open all other ele­ments on the page are non-clickable. Clos­ing the modal is also ani­mated, with a min­imise effect. I’ve marked up the modal using <aside>, but depend­ing on the pur­pose of yours, <nav> or prob­a­bly <details> might be more appropriate.

Of course, using images and JS will only make the modal bet­ter, and some­thing like hit­ting ESC to close will never be repro­duced in CSS. Pure CSS is rarely the best production-ready solution.

How to

The :tar­get pseudo-selector changes the style of a tar­geted ele­ment. Com­bin­ing a link point­ing to an ele­ment with :tar­get and alter­ing visibility/display/opacity gives a hide/show mech­a­nism. To facil­i­tate the ani­ma­tions, which were jerky when using display:none, I’ve used a com­bi­na­tion of :tar­get, opac­ity and pointer events:

.modal {
opacity: 0;
pointer-events: none;
}

.modal:target {
opacity: 1;
pointer-events: auto;
}

The modal is two parts, one part con­tainer, one part con­tent. Ide­ally the con­tainer would be gen­er­ated using a pseudo-element, but I haven’t got that work­ing yet. The con­tainer spreads across the whole page and dims the back­ground with rgba. A high z-index puts the modal on top.

.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
…
}

The con­tent is posi­tioned roughly in the mid­dle and is pret­ti­fied with a sprin­kling of text shadow, bor­der radius, box shadow and gradient.

There are two ani­ma­tions, one named “bounce” (scale to slightly larger than nor­mal, then fall back) and another, “min­imise”, which act on the con­tent part. These com­bine with a sep­a­rate opac­ity tran­si­tion on the container.

The sim­ple opac­ity transition:

.modal {
…
-webkit-transition: opacity 500ms ease-in;
-moz-transition: opacity 500ms ease-in;
transition: opacity 500ms ease-in;
}

The scal­ing ani­ma­tions, although only 2D, uses scale3d for hard­ware accel­er­a­tion. To make the bounce more real­is­tic box shadow is also ani­mated, which comes with a per­for­mance hit. Show­ing only the webkit ver­sion for brevity:

@-webkit-keyframes bounce {
  0% {
  	-webkit-transform: scale3d(0.1,0.1,1);
  	box-shadow: 0 3px 20px rgba(0,0,0,0.9);
  }
  55% {
  	-webkit-transform: scale3d(1.08,1.08,1);
  	box-shadow: 0 10px 20px rgba(0,0,0,0);
  }
  75% {
  	-webkit-transform: scale3d(0.95,0.95,1);
  	-box-shadow: 0 0 20px rgba(0,0,0,0.9);
  }
  100% {
  	-webkit-transform: scale3d(1,1,1);
  	box-shadow: 0 3px 20px rgba(0,0,0,0.9);
  }
}

@-webkit-keyframes minimise {
  0% {
  	-webkit-transform: scale3d(1,1,1);
  }
  100% {
  	-webkit-transform: scale3d(0.1,0.1,1);
  }
}

To change the ani­ma­tion on open we can use the cas­cade and over­ride the default ani­ma­tion with a more spe­cific one, using :tar­get again:

.modal > div {
…
-webkit-animation: minimise 500ms linear;
}

.modal:target > div {
-webkit-animation-name: bounce;
}

The close but­ton is a hid­den close link with a styled ::after pseudo-element that scales on hover/focus/active. As we’re hid­ing the orig­i­nal close link, there are some hoops to jump through to make the :focus state change on the pseudo-element. The tra­di­tional clip, text indent or vis­i­bil­ity hid­den meth­ods all fail, and I’ve resorted to color:transparent and some spe­cific focus styles to over­ride the con­fused native ones.

.modal a[href="#close"] {
…
color: transparent;
}

.modal a[href="#close"]:after {
content: 'X';
display: block;
…
}

.modal a[href="#close"]:focus:after,
.modal a[href="#close"]:hover:after {
-webkit-transform: scale(1.1,1.1);
-moz-transform: scale(1.1,1.1);
}

.modal a[href="#close"]:focus:after {
outline: 1px solid #000;
}

Caveats

It won’t work in IE8 and below, there’s no pointer-event sup­port and opac­ity is poorly imple­mented. IE9 sup­ports :tar­get but no pointer-events. Some IE spe­cific styles could eas­ily switch the opac­ity tog­gle to a dis­play or vis­i­bil­ity one.

Users will still be able to tab through the links in the back­ground and acti­vate them. This only becomes an issue if you ignore the focus state…

As I alluded to at the start, some JS hooks for key­board inter­ac­tion wouldn’t go amiss, ESC to close and some­thing to pull focus to the cur­rent modal and then back again on close.

The close but­ton would prob­a­bly look bet­ter with an image and the markup uses an extra con­tain­ing ele­ment, which is always undesirable.

Ani­ma­tions can be great, in small quan­ti­ties, but some­thing that ani­mates too much and gets in the way of func­tion­al­ity is a major draw­back for users. It would be quite easy to go over­board with this.

I’ve added a fol­low up post that addresses some of the caveats and makes this a lit­tle more cross browser com­pat­i­ble: CSS Modal Fol­low Up.
Paul Hayes

Paul Hayes is a developer at Last.fm. You should follow him on Twitter, where he talks about UX, HTML, CSS and JavaScript, amongst other cool stuff.