Draggable Metro App Showcase

Today I'd like to share with you an interactive and touch-optimized metro app showcase concept for showcasing a metro (probably a Windows Phone) app screenshot. The screenshot will be draggable and swipable, and you'll have a couple of extra options to view how the app would look like in a mobile phone frame.

Please note that this demo works only in browsers that support the Javascript objects and APIs used. I provided a couple of polyfills but the demo will only work in browsers that these polyfills provide fallback for. See the Javascript section for details.

This demo is inspired by the big number of dribbble shots showcasing Windows phone app concepts, so I thought I'd recreate this showcasing concept and add some interactivity to it.

The Flat Lumia Phone PSD Mockup used in the demo is by Corey Ginnivan from Dribbble. I provided two colors in the demo resources, a red and a white frame.

We'll wrap our showcase in a as-wrapper wrapper, which will contain a container for the mobile frame + app screenshot, and a section for the app description which will appear at some point during the interaction (we'll get to that in a moment).

The mobile frame and the screenshot will be positioned absolutely. The frame needs to be positioned absolutely to overlap the screenshot, and the screenshot will be positioned this way too so that we can change its position via Javascript.

The phone frame we're using has 3 buttons in its lower section, what we're going to do is we're actually doing to add 3 buttons on top of these buttons with a transparent background, so that it seems like these built-in buttons are clickable. And then we're also going to add two navigation arrows to the right of the frame to scroll the screenshot left and right.

The left-most arrow on the phone frame will scroll the app screen to the left to get it completely inside the boundaries of the phone frame. The windows button will scroll it back out to its initial position. The magnifier will launch the "focus" mode of the showcase, and the left and right navigation arrows will scroll the screenshot left and right respectively.

<div class="as-wrapper">
<div class="as-container">
<div class="as-frame preventSelect" id="as-frame">
<img src="../../assets/images/lumia-red.png" alt="Omnia Phone Frame" />
</div>
<div class="as-instructions" id="as-instructions">
<p>Drag or swipe app screenshot left and right with your mouse or finger.</p>
<p>Use buttons at the bottom of the frame to scroll screen and focus mobile frame.</p>
<button>Got it!</button>
</div>
<div class="as-nav-buttons" id="as-nav-buttons">
<button class="as-button as-nav-button as-left-nav-button preventSelect" id="as-left-nav-button"><img src="../../assets/images/nav-arrow-left.png" alt="Left"></button>
<button class="as-button as-nav-button as-right-nav-button preventSelect" id="as-right-nav-button"><img src="../../assets/images/nav-arrow-right.png" alt="Right"></button>
</div>
<button class="as-button as-slide-button preventSelect" id="as-slide-button"></button>
<button class="as-button as-reset-button preventSelect" id="as-reset-button"></button>
<button class="as-button as-focus-button preventSelect" id="as-focus-button"></button>

<div id="draggable" class="as-screen preventSelect">
<img src="../../assets/images/app-screen.jpg">
</div>
</div>
<div class="as-app-description preventSelect" id="as-app-description">
<h2>Your awesome app features and upsell</h2>

<p>Minima vero quibusdam error accusamus explicabo commodi deleniti ipsa debitis enim quae tempore molestias veritatis. Quo saepe voluptatibus officiis debitis necessitatibus magnam id possimus maxime atque amet. Officiis cupiditate deserunt!</p>
<p>Minima vero quibusdam error accusamus explicabo commodi deleniti ipsa debitis enim quae tempore molestias veritatis.</p>
<p>Minima vero quibusdam error accusamus explicabo commodi deleniti ipsa debitis enim quae tempore molestias veritatis.</p>
<a href="#">
<img src="../../assets/images/ws-button.png" alt="Download App from Windows Store" class="download-button" id="download-button" />
</a>
</div>
</div>

You have probably noticed the class preventSelect that I adde to almost all elements, especially those inside the as-container. What this class does is that it prevents these elements (via CSS) from being selected, otherwise selected elements will get in the way of the drag action and things will get messy!

I'll go over the styles quickly. All styles are basic and easy to understand so I won't be getting into too much detail. The "heart" of this demo is the Javascript part, the CSS is simple and pretty straightforward. I added comments to the CSS code where necessary. We'll start with the general styles relevant to the demo.

/* lazy reset :) */
*{
/*box sizing should be border box on .as-frame and .as-screen otherwise js calculations will need to change to include padding*/
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
list-style: none;
}
body {
background: #F0E9DD url("../../../assets/images/02.jpg") repeat;
color:#eee;
font: 300 1.2em "Source Sans Pro", sans-serif;
overflow-x: hidden;
}
/* cross-browser prevent user select: http://stackoverflow.com/a/4358620 */
.preventSelect {
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.as-wrapper{
width:95%;
margin:0 auto;
min-height:550px;
position:relative;
}
.as-container {
position: relative;
height: 550px;
width: 1300px;
overflow: hidden;
margin:20px auto;
transition: width .6s ease;
}
/*class will be added via Javascript to shrink the frame + screenshot container and center it*/
.shrink{
width:297px;
}

So far all styles are obvious and straight forward. The frame and screenshot container is given a height equal to the height of the phone frame (simply because we don't need it to be bigger than that), and now we'll move on to the frame and screenshot styles.

/*div containing the app screenshot*/
.as-screen {
height: 75.6%;
width: 1190px;
top: 8.5%;
left:0;
position: absolute;
cursor: move;
cursor:grab;
cursor:-moz-grab;
cursor:-webkit-grab;
z-index: 1;
overflow: hidden;
transition: all .5s ease-out;
}
/*app screenshot*/
.as-screen img{
pointer-events:none;/*to prevent image being dragged and interfering with the screen drag*/
}
/*div containing the phone frame*/
.as-frame {
position: absolute;
z-index: 1;
left: 0;
width: 300px;
height:550px;
z-index: 2;
pointer-events:none;
}
/*the phone frame*/
.as-frame img{
width:100%;
pointer-events:none;
}
.as-instructions{
position: absolute;
top:100px;
left:50px;
width:200px;
padding:20px;
color:white;
background:rgba(0,0,0,0.75);
z-index:20;
pointer-events:none;
}
.as-instructions button{
background: none;
border:none;
background-color: #B33E41;
color:white;
padding:5px 10px;
margin-top:15px;
}

Note that we need to set the pointer events on the frame to none to make sure it doesn't block the events on the screenshot.

Now we'll style and position the control and navigation buttons.

.as-slide-button, .as-reset-button, .as-focus-button{
width:40px;
height:40px;
position:absolute;
bottom:48px;
left:30px;
background:none;
border:none;
cursor:pointer;
z-index:20;
}
.as-reset-button{
left:130px;
}
.as-focus-button{
left:225px;
}
.as-nav-buttons{
height:30px;
position:absolute;
bottom:30px;
left:320px;
z-index:20;
}
.as-nav-button{
width:40px;
height:40px;
background:none;
border:none;
color:black;
text-align:center;
cursor:pointer;
}

Last thing we're going to style is the description section which will appear when the screenshot has been dragged fully into the inside of the phone frame.

/*initially the description will be hidden with opacity set to 0*/
.as-app-description {
opacity: 0;
width:100%;
position:absolute;
right:0;
top:0;
bottom:0;
margin: 5% 0;
padding: 0 50px 0 450px;
transition: opacity .3s ease-out;
}
.as-app-description h2{
margin-bottom:1em;
}
/*this class will be added via Javascript to show the description*/
.visible-description {
opacity: 1;
}
.download-button {
margin: 30px 0;
}

We'll make the demo as responsive as possible. I'm saying "as responsive as possible" because a draggable showcase like this will look best on big/desktop screens, because of the width of the screenshot, but we'll make it work for all screen sizes. :)

@media screen and (max-width: 64em){
.as-app-description{
padding-left:380px;
}
}
@media screen and (max-width: 50em){
.as-wrapper{
padding:1%;
}
.as-app-description{
position:static;
margin-top:100px;
padding:0;
opacity:1;
}
}

@media screen and (max-width: 30em){
.as-container{
width:297px;
margin:30px auto;
}
}

On small screens we'll let the screenshot remain inside the phone frame with overflow set to hidden on the container.

That's pretty much it for the styles. Now let's move on to the interactive part!

OK, first things first: polyfills and plugins. For starters, I won't be using any JS framework, we'll be going vanilla.

I'll be using the awesome Javascript classList API, which is not fully supported in all browsers, but it's awesome so I'll be using it anyway, and I'll add Eli Grey's classList polyfill which works in IE8, and provides basic classList.add(), classList.remove(), and classList.toggle() support (which is more than enough for this demo) to at least as far back as Android 2.1.

For browsers that don't support addEventListener, I'll be using Jonathan Neal's eventListener polyfill.

Finally, I'll be using Hammer.js to add touch swipe support for the draggable screenshot.

We'll start by caching some variables and initializing others with some basic calculations which we'll need throughout the code.

              (function(){
                  var el = document.getElementById('draggable'),
                    //get screen width and offset..
                      elWidth = parseInt(window.getComputedStyle(el,null)['width']),
                      elLeft = el.offsetLeft,
                    //..use those to calculate right offset
                      elRight = elLeft + elWidth,
                    //do the same for the phone frame
                      frame = document.getElementById('frame'),
                      frameLeft = frame.offsetLeft, 
                      frameWidth = parseInt(window.getComputedStyle(frame,null)['width']),
                      frameRight = frameLeft + frameWidth,
                    //cache app description and control and navigation buttons 
                      desc = document.getElementById('as-app-description'),
                      scrollInButton = document.getElementById('as-slide-button'),
                      resetButton = document.getElementById('as-reset-button'),
                      focusButton = document.getElementById('as-focus-button'),
                      leftNavButton = document.getElementById('as-left-nav-button'),
                      rightNavButton = document.getElementById('as-right-nav-button'),
                    //instruction that appears at beginning of demo
                      tip = document.getElementById('as-instructions'),
                    //cache container
                      container = el.parentNode;
            ```

wow that's a lot! so what exactly are all those needed for?

First, I cached all DOM elements that we're going to listen for events on so we can attach event handlers to them next. Then, I determined the left and right offsets for each of the draggable screen and the mobile frame, because we'll be needing these for the scrolling and dragging functions. The right offset is calculated by adding the left offset to the width of the element.

Next, we'll attach event listeners to the control and navigation buttons, and we'll also add the swipe support with Hammer.js.

              //call the scrollScreen function when the screen is swiped left or right
              var scrollLeftOnSwipe = Hammer(el).on("swipeleft", function(event) {
                  scrollScreen(220, 'left');
                  hideTip();
              });
              var scrollRightOnSwipe = Hammer(el).on("swiperight", function(event) {
                  scrollScreen(220, 'right');
                  hideTip();
              });

              scrollInButton.addEventListener('click', function(){
                  scrollScreen(elWidth, 'left');
              }, false);
              leftNavButton.addEventListener('click', function(){
                  scrollScreen(220, 'left');
              }, false);
              rightNavButton.addEventListener('click', function(){
                  scrollScreen(220, 'right');
              }, false);
              resetButton.addEventListener('click', resetScreen, false);
              focusButton.addEventListener('click', focusFrame, false);
            ```

The scrollScreen(val, dir) function takes in two arguments: a val which is the amount (in px) by which we want to scroll the screen, and a dir which determines the direction in which we want to scroll it.

              function scrollScreen(val, dir){
                hideTip();
                var left = el.offsetLeft;

                if(dir == 'left'){
                    var deltaRight = elRight - frameRight;
                    if(deltaRight >= val){
                        left -= val;
                    }
                    else{
                        left -= deltaRight + 5;
                    } 
                }
                else if(dir == 'right'){
                    var deltaLeft = frameLeft - left;
                    if(deltaLeft >= val){
                        left += val;
                    }
                    else{
                        left += deltaLeft;
                    }
                }

                if(left <= frameLeft && elRight >= frameRight - 5){
                    el.style.left = left + 'px';
                    elRight = left + elWidth;// in case elRight = frameRight the desc shows
                    showHideDesc();
                }    
            }

            function showHideDesc(){
                if( elRight <= frameRight + 30 && !focus){
                    desc.classList.add('visible-description');
                }
                else{
                    desc.classList.remove('visible-description');
                }
             }

             function hideTip(){
                tip.style.display= "none";
            }

             //when the reset button is clicked the screen is returned to its start position
             function resetScreen(e){
                el.style.left = 0;
                elLeft = 0;
                elRight = elWidth;
                showHideDesc();
            }
            ```

The function calculates the difference between the screenshot offsets and that of the frame offsets, and scrolls the screen by the value passed to it as long as the difference is bigger than this value. If it's smaller, it scrolls it by the value of the difference. At the end of the function, another function showHideDesc() is called, which shows and hides the app description section based on the position of the screenshot with respect to the frame: if the screenshot's right offset = that of the frame's right offset, i.e the screenshot is fully inside the frame, then the description is shown, else, it's hidden.

When the left arrow button (the one on the phone frame) is clicked, the scroll function is called with a value equals to the width of the screenshot, which basically means: scroll the screen to the max until it's fully inside the frame.

The focus button (the magnifier) will cause a mode change for the demo. When it is clicked, the container containing the phone frame and the app screenshot will shrink (by adding the .shrink class to it) to fit the size of the frame, and it's overflow is hidden, and it's centered in the screen, this way the frame will contain the app screenshot and you can drag/swipe left and right to view the app inside of it. (see image below)

The app showcase in 'focus' mode.
              var focus = false;

              function focusFrame(){
                  hideTip();
                  if(focus == false){
                      container.classList.add('shrink');
                      focus = true;
                      //show/hide description based on whether we're in the 'focus' state or not
                      desc.classList.remove('visible-description');
                  }
                  else{
                      focus = false;
                      container.classList.remove('shrink');
                      el.style.left = '0';
                      elRight = elWidth;//so that the description remains hidden
                  }
              }
            ```

The last thing we're going to do is add the drag functionality to the app screen. We'll be attaching event handlers for mousedown, mousemove, and mouseup events, and their equivalent touchstart, touchmove, and touchend events to support touch devices.

What will happen is that every time the mouse is down (i.e the drag starts), the position of the mouse/finger is saved, and the current left offset of the screen is calculated, and a value delta is also calculated, which determines the difference between the mouse position on drag start and the left offset of the draggable element (app screen).

After that, as the mouse moves, its position is updated, and as its position changes so will the left offset of the draggable screen, as long as the boundaries of the screen don't exceed the boundaries of the frame from the left and right respectively: the right offset of the screen should not go below the right offset of the frame, and the left offset of the screen should not go above the left offset of the frame.

Now that we've cleared up the logic behind the dragging function, here's the code for that function.

              //these values are reset on every mousedown event
              var mouseDownStartPosition, delta, mouseFrameDiff; 

              el.addEventListener("mousedown", startDrag, false);
              el.addEventListener("touchstart", startDrag, false);

              function startDrag( event ) {
                  hideTip();
                  //prevent contents of the screen from being selected in Opera and IE <= 10 by adding the unselectable attribute
                  el.setAttribute('unselectable', 'on');

                  elLeft = el.offsetLeft,
                  mouseDownStartPosition = event.pageX,
                  delta = mouseDownStartPosition - elLeft;
                  
                  document.addEventListener("mousemove", moveEl, true);
                  document.addEventListener("mouseup", quitDrag, false);
                  document.addEventListener("touchmove", moveEl, true);
                  document.addEventListener("touchend", quitDrag, false);
              }

              function moveEl(e){
                var moveX = e.pageX,
                    newPos = moveX - delta;
                    elLeft = newPos;
                    elRight = newPos + elWidth;
                    
                //-5 is a magic number because the phone frame has extra 5 px on the right side with a transparent bg
                //if you're using a different phone frame img u may not need this, but keeping it won't do any harm :)
                if(elRight >= frameRight - 5 && elLeft <= frameLeft){
                    el.style.left = newPos + 'px';
                    showHideDesc();
                }
             }

             function quitDrag(){
                 document.removeEventListener('mousemove', moveEl, true);
                 el.setAttribute('unselectable', 'off');
             }
            ```

To make sure the screen doesn't keep moving when the dragging stops, we attached an event handler to the mouseup (and touchend) event, that will call a function which in turn will remove the corresponding event handlers from the mousedown and mousemove events.

And that's it, I hope you like this showcase and find it useful! :)



Level up your accessibility knowledge with the Practical Accessibility course!

I created a self-paced, get-right-down-to-it online video course for web designers and developers who want to start creating more accessible Web user interfaces and digital products today.

The course is now open for enrollment!

Real. Simple. Syndication.

Get my latest content in your favorite RSS reader. (What is RSS?)

Follow me on X (formerly Twitter)