I got a new Apple TV for my birthday this year, after our old one kept annoyingly crashing right in the middle of the third season of Orange is the New Black. It’s got an all new UI and a touch sensitive remote.

In typical Apple fashion, apps are displayed in a grid, but unlike iOS, there are some very cool subtle effects as you navigate through the app grid. I was pretty sure that these effects were possible in CSS and supported in all modern browsers these days, so I gave it a go.

Try it out!

The Code

HTML

<div class="app-icon">
  <div class="layers">
    <div class="light-effect"></div>
    <div class="layer layer-3"></div>
    <div class="layer layer-2"></div>
    <div class="layer layer-1"></div>
    <div class="layer layer-0"></div>
  </div>
</div>

The HTML structure is really simple. The .app-icon element is a container for all the elements (which in this case are layers) that build up the app icon. This element has the perspective CSS property applied to it, which will create the 3D effect. It’s important to understand that this property affects the children of the element and not the element itself. The layers that build up the icon is in another container with the class of layers. This is the element that will be targeted with JavaScript when the mouse hovers over it. Finally, there is an element for each of the layers that build up the icon, and an element for the lighting effect.

The Sass (CSS)

I created the styles for the app icon by using a CSS pre-processor called Sass. Since I started using it a couple of years back, I cannot imagine ever using a project without it. If you haven’t checked it out yet, I recommend you do!

.app-icon
  display: inline-block
  width: 300px
  height: 180px
  perspective: 1000px

The .app-icon element is pretty standard stuff. The display is set as inline-block so that when multiple icons are displayed on screen, they are rendered nicely as a grid. The most important rule here is perspective. This puts the element in a 3D space. You can see this effect when rotating the element on the X or Y axis. The greater the value of this property, the further the user is from the Z plane and the less pronounced the effect is. I experimented with this property a bit to get the effect right.

.layers
  height: inherit
  width: inherit
  position: relative
  overflow: hidden
  transform: translateZ(-3rem)
  border-radius: 4px
  box-shadow: 0 30px 50px 0 rgba(0,0,0,0.3)

The .layers element is the container of all the elements that make up the app icon. There are a few rules here that are key to creating the tvOS icon style. First off, the position: relative rule allows us to position the different layers of the icon accurately. The parallax effect requires us to sometimes have to make the layer element larger than its parent (explained later) in order to create a safe zone. In order for the layers to not overflow the boundaries of the container, we add the rule overflow: hidden. The border-radius has a small value, just to modestly enhance the look of the icon. The two most crucial rules that create the 3D effect is the box-shadow and the transform. The box-shadow creates the effect that this icon is lifted from the page. The transform: translateZ(-3rem) rule pushes the icon backwards slightly. This seems to change the origin of where the icon is rotated from, which is slightly “in front” of the icon. This gives a tilting effect that is more in line with that of tvOSs.

.light-effect
  position: absolute
  top: -30rem
  left: 0rem
  height: 400px
  width: 400px
  border-radius: 400px
  background: radial-gradient(ellipse at center, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 70%,rgba(255,255,255,0) 100%)
  z-index: 999

The .light-effect element should be one of the standard elements within .layers. This provides the lighting effect that is present on all the tvOSs icons. The “shine” that appears on the app icon is simply a radial gradient background going from a slightly transparent white to fully transparent. When looking at the example in Apple’s Human Interface guidelines, you can see that the shine effect is actually quite big. So the height and width of this element is actually bigger than the icon itself (the overflow: hidden rule of its parent element ensures that the shine isn’t visible outside the app icon boundaries). The effect doesn’t appear until the top of the icon is tilted away, so the position of the shine (by default) is top: -30rem placing it above the icon. The z-index value ensures that the shine is always in front of the other layers so that it is always visible.

.layer
  position: absolute

Each element that makes up the app icon has a class of layer, which has a single rule of being absolutely positioned, so that it can be placed relative to the .layers container. Generally, the .layer elements will probably also have a second class, which will specify additional rules for the layer.

.layer-0
  position: absolute
  height: 100%
  width: 100%
  padding: 2rem
  top: -2rem
  left: -2rem
  background: #3498db
  z-index: 0

In this example, the first layer of the icon is the background. You may notice that there is padding on this element when the height and width of the element are already 100% of it parent, meaning that its size is greater than the element it is contained in. This is something that Apple calls the safe zone. During parallax, the elements inside the icon will move with the motion of the icon, so an extra bit padding will ensure that the outer edges of the background are not visible inside the icon when the element is moved.

.layer-1
  height: 60px
  width: 60px
  border-radius: 50px
  background: #f39c12
  z-index: 1
  top: 1rem
  right: 1rem

.layer-2
  bottom: 0
  right: 0
  width: 0
  height: 0
  border-left: 120px solid transparent
  border-right: 120px solid transparent
  border-bottom: 120px solid #27ae60
  z-index: 2

.layer-3
  bottom: 0
  left: -1rem
  width: 0
  height: 0
  border-left: 100px solid transparent
  border-right: 100px solid transparent
  border-bottom: 100px solid #2ecc71
  z-index: 3

The app icon should always have at least one layer, but you could theoretically have as many layers as you want. However, if you want a parallax effect, you will need multiple layers. The CSS classes you see defined above are styles specific to each layer.

The JavaScript

A small word of caution before I get on the JavaScript. I am by no means an expert, and this example may not be written in the best, most efficient way possible. Saying that, the effect is nice and smooth on every browser and OS I’ve tested it on.

So here it is!

$(document).ready(function() {

  $(".layers").mousemove(function(e) {
    var parentOffset = $(this).parent().offset();
    var relX = e.pageX - parentOffset.left;
    var relY = e.pageY - parentOffset.top;
    var amountOfLayers = $('.layer').length;

    $(this).css("transform", "rotateY(" + ((relX - 150)) / 16 + "deg) rotateX(" + ((relY - 90) / 12) + "deg) translateZ(-3rem)");
    $(this).find('.light-effect').css("transform", "translateY(" + (relY - 50) * 3.5 + "px) translateX(" + ((relX - 100) * - 1.2) + "px)");

    $(this).find(".layer").each(function(i) {
      $(this).css("transform", "translateX(-" + ((relX / 20) / (amountOfLayers - (i + 1))) + "px) translateY(" + ((relY / 20) / (amountOfLayers - (i + 1))) + "px)");
    });
  });

  $(".layers").mouseleave(function() {
    $(this).css("transform", "rotateY(0) rotateX(0) translateZ(-3rem)");
    $('.light-effect').css("transform", "translateY(0)");
    $(".layer").css("transform", "translateX(0) translateY(0)");
  })

});

The JavaScript is where the icon effect comes to life. The JavaScript handles mouse movement over the icon and depending on where the mouse position is, will rotate the icon on the X and Y axis, move the lighting shine effect over the icon and also move the elements within the icon, creating a parallax effect.

We do this by binding an event handler to the target. In this case we are using the mousemove event on any element with the class of layers. The handler is an anonymous function which is executed every time the event is triggered (whenever the mouse moves over the element). The function takes an Event object as a parameter which can be used to determine the X and Y coordinates of the mouse. Here, I use it in conjunction with the coordinates of the parent element relative to the document to determine what part of the icon that the mouse is moving over.

Next, we adjust the CSS of the of the .layers element (we can use $(this) which refers to the current selected object in an event). We can use the .css() method to manipulate the value of the transform property in real-time, using rotateX and rotateY to rotate the icon on both X and Y axises based on the position of the X and Y coordinates of the mouse cursor. We also have to use translateZ(-3rem) in order to preserve the icons position in the Z-axis so that is does not jump forward when the mouse moves over it. There is some cool looking maths going on in these lines of code. I wish I could explain the logic behind behind the maths used to determine how many degrees to rotate on the X and Y axis, but to be totally honest, it was a case of tweaking, random guesswork and trial and error! I plan to rewrite this section and make sense of it very soon!

For the lighting effect, I used the .find() method to find the element with a class of light-effect and used the .css() method again to change the position of the lighting effect depending on the position of the cursor. The lighting effect does not appear on the icon until the cursor is about halfway down the icon, and it appears in the part of the icon which is tilted closest to the user. I had to tweak the maths to work out the the pixel value for the translateX and translateY rule, and again, it was a case of trial and error and a bit of guesswork. The X and Y coordinates of the mouse is multiplied so that the lighting effect moves quicker than the mouse position.

Now it’s time to create the parallax effect. Each layer that makes up the icon has a class of layer. This allows us to iterate over each layer in the icon and adjust the position of them by different amounts depending on its position on the z-axis. The parallax effect is quite subtle so the X and Y coordinates of the mouse cursor is divided by 20 so that the movement of the layer is small. This is also divided by the amount of layers in the icon minus the current iteration (plus one, as it starts at zero). This makes the layers move at different rates, achieving the parallax effect.

Finally, when the mouse cursor leaves the icon, it is time to reset everything back to the way it was. We use the mouseleave() event on the .layers element for this. Here, I simply set any rotation and positioning back to zero. :-)