Cloning the UI of iOS 7 with HTML, CSS and JavaScript

09/6/2013

 

Disclaimer: This article is heavily based on iOS 7 beta 6, so things might change until the final version.

There’s been a lot of controversy recently on whether Javascript could compete with native or not, both in terms of performance and of look’n’feel. As I had a few hours of train ahead of me this summer, I thought I could try to make an app that looks as close as a native iOS 7 app as possible. This led me to develop Dedicards, a very simple app to send gorgeous virtual postcards.

As Dedicards is currently held in the « waiting for review » purgatory state of iTunes Connect because the App review team thinks it resembles too much their beloved and top-secret (oh wait…) iOS 7, I’ve had plenty of time to write this in-depth article about what I learned.

In order to provide an example alongside, you will find a sample app (well, not really an app, more a demo made of a couple of views) in this Github (live demo here). I didn’t know how to name it, so I took the liberty to pay a tribute to the Apple reviewer who didn’t want to validate my app by naming it « Project Tyson ». If he reads me, I hope he doesn’t take it the wrong way.

This article only takes into account the iPhone, the iPad’s behavior being another story. In this document, I make an extensive use of two amazing and must-read articles: the iOS 7 Cheat Sheet by Ivo Mynttinen and “How I built the Hacker News mobile web app” by Lim Chee Aun.

 

1. HTML Code & CSS

1.1. General structure

In this project, I tried to recreate the basic “Navigation view” component, consisting of a content area and a navigation bar (which displays the view’s title and some buttons to navigate through the app).

view1
A very basic illustration of how the Navigation view works

Basically, the view is divided between a navigation bar docked at the top and a content area that scrolls all across the view. This is a major difference in iOS 7 compared to previous versions: scrolled content remains visible behind the navigation bar with a nice translucency/blur effect. I’ll come back to that later but this hugely complicates the task of recreating the iPhone’s UI in HTML/CSS (Note: Apple let developers to choose whether or not to activate this translucency effect, so it would be perfectly acceptable to set an opaque background).

 

1.2. Fonts & color

iOS 7 introduced a widely criticized Helvetica Neue Ultra Light typeface. Even being a big fan of light typefaces, I have to admit it was rather difficult to read on non-retina screens. Since its newest releases, iOS 7 switched to a bolder typeface, much easier to read. Basically, I found out that it was either using a font-weight:300; or font-weight:500; depending on whether it was a bold element or not.

As for the color, I used screenshots and Photoshop to determine appropriate rgb codes:

 

1.3. Navigation bar

The navigation bar is 44px tall (88px retina-wise) and has a width of 100%. In order to center the view’s title vertically in the navbar, I added a line-height:44px; property as well. In order to understand what elements compose this navigation bar, here is a sample HTML code:

<header><button class="left arrow">
<div class="label">{Left Button Label}</div>
&nbsp;

&nbsp;

</button>
<h1>{View Title}</h1>
&nbsp;
<div class="label">{Action Button Label}</div>
&nbsp;

</header>

As its name suggest, the purpose of a navigation bar is to provide a way to navigate inside the app (i.e. by sliding views from right to left or from left to right). In order to do so, it uses navigation buttons, which look like this:

actbtn
(example of a navigation button in the Contacts app)

Additionally, it can also feature what I call “action buttons”, without an arrow, which purpose is to call other views (for instance by making them slide from bottom to top, like when you compose a new email) or notifications (alert, confirm, etc.). They look like this:

navbtn
(example of an action button in the Contacts app)

In this case, I chose to distinguish the two by adding a .arrow class to the button when needed. Furthermore, buttons’ labels are in a separate div for animation purposes (covered in section 2).

<button class="left arrow"></button>

An example of a navigation button

I also added a max-width:140px in order that no button can take more than half the navigation bar. If the label is too long, an ellipsis is added automatically thanks to the text-overflow: ellipsis; property.

By default, a padding of 9px is automatically applied on the edge. Yet, if the button receives the .arrow class, an image background (the arrow) is added and padding is increased to 23px.

Finally, there is the question of translucency. In iOS 7, the navigation has a semi-transparent background. This is fairly easy to implement in CSS with a background-color: rgba(248, 248, 248, 0.9); property. Still, the content behind the navigation bar is supposed to be blurred. The problem is I don’t see how to do this without compromising the performances or the looks. Hence I’ve decided to stick to the background opacity solution. If you’re really uncomfortable with this solution, you can simulate an opaque navigation bar (which is acceptable in a native app by setting the translucent property to NO in XCode) by changing the background-color property to rgb(248, 248, 248).

 

1.4. Content area

As I said before, the content area now scrolls behind the navigation bar, so it takes 100% of the view. In his article, Lim Chee Aun explained how he got the content area to scroll seamlessly in just a portion of the view, thanks to a div (.scroll) with a -webkit-overflow:touch CSS rule containing another div (.content) with the actual content:

<div class="scroll">
<div class="content">{Content}</div>
</div>

Still, as the the content now takes 100% of the view, I tried to make it scroll normally and to assign a position:fixed; to the navigation bar. But it didn’t work out well with the animations and I noticed glitches in the navigation bar while scrolling.

As a result, I decided to stick to Lim Chee Aun’s solution. First, I disabled scrolling on the html and body elements. Then, I positioned the content area and the header as follow to make sure they are both correctly staying at the top of the screen:

header { position: absolute; top:0; }
.scroll { position: absolute; top:0; overflow: auto; -webkit-overflow-scrolling: touch; }

Finally, each element has to be layered properly, so we use a z-index property in order to make sure that the content scrolls behind the navigation bar:

header { z-index:10; }
.scroll { z-index:1; }

The content area can be styled as you wish but it comes with list and forms elements predefined. Basically, they are made of a simple grey border at the bottom with a margin-left of 15px (30px retina-wise) and a padding-right of 15px as well (so they look off-centered).

 

1.5. Final review

Here is our code snippet for each view (don’t worry about the .scrollMask div, I will explain its purpose in section 2):

<div class="view" id="view-{id}"><header><button class="left arrow">
<div class="label">{Left Button Label}</div>
&nbsp;

&nbsp;

</button>
<h1>{View Title}</h1>
&nbsp;
<div class="label">{Action Button Label}</div>
&nbsp;

</header>
<div class="scrollMask"></div>
<div class="scroll">
<div class="content">{Content}</div>
</div>
</div>

While pretty straightforward, I’m afraid this structure could lead to an unmanageable code if there are more than 10 views. If such is the case, I think the code could be streamlined to just two views dynamically loading JS templates.

 

2. Animations

2.1. How it works

Animations are a tricky part. If we consider the common sliding animation, iOS doesn’t simply slide from one view to another. There are no less than six different animations occurring at the same time to change views and those differ depending on the type of buttons (navigation or action).

Let’s consider a transition from right to left. Here is what we can see:

Now, there’s a subtlety in case of navigation buttons (those with arrows). In previous versions of iOS, navigation buttons were sliding with the rest. In iOS 7, the title of the view that is sliding out literally fades into the navigation button:

As images are much clearer than words, here’s a quick schema:

screencast
400ms of animation in 8 screenshots (click to see it full size).

Finally, the last kind of animation is a view that scrolls from bottom to the top or vice-versa. This one is rather easy to implement since the whole view (content, navigation bar, etc.) slides all at once from the bottom edge to the top.

 

2.2. CSS

Now that we have decomposed the animation, let’s translate that in CSS. Basically, each element is animated with a @-webkit-keyframes and we enable GPU acceleration thanks to the translateX property. The CSS code is rather straightforward, there’s just a trick with the -webkit-animation-timing-function property.

iOS doesn’t use any standard easing, so I tried to mimic that with a Bezier curve (cubic-bezier).

cubicbezier
The bezier curve I used, from the Cubic Bezier Builder made by Rob La Placa. It goes very fast and then progressively slows down to recreate this sensation of smoothness we can find all across iOS 7.

Yet this Bezier curve is not appropriate for our buttons. A close look at their behavior in the beta shows they fade in very slowly and then much faster when the rest of the elements are almost in place. As a result, I just changed the cubic-bezier parameters for the button in order to slow the animation in the beginning and accelerate it in the end. Buttons that are fading out just behave symmetrically in order to make a smooth transition.

Finally, in the pure Apple fashion, there’s a one last thing. The content area, while scrolling, has a light drop-shadow. Because .scroll remains visible behind the navigation bar and because .content can be scrolled (and therefore also be visible behind the navigation bar), I had to add another div, .scrollMask. This div has just one purpose: adding a drop-shadow behind the content during the animation.

As this drop shadow fades out while transitionning, the -webkit-drop-shadow parameter just change the alpha channel of its rgba value. Besides, the .scroll that is sliding out has to get darker. As a result, during the whole transition, the .scrollMask behind the sliding-out content has a black background and .scroll changes in opacity from 1 to 0.9.

 

2.3. Javascript

In order to trigger these animations and to change views, we have to rely on a Javascript function. Basically, the Slide() function adds transition classes to the appropriate divs and subscribes to the webkitAnimationEnd event. Once the animation is over, it hides the view that has slided out and removes transition classes.

This function takes three mandatory parameters and one optional parameter:

If you dig in the app.js file, you can see a slideOpts object that is used to match the desired transition to the appropriate CSS classes. For instance sl matches to the .slin class for the view that sliding in and to the .slout class for the view that is sliding out.

 

3. Other Bits

3.1. iOS 6, Retina, non-Retina and native

This code has been thought for an in-browser app but thanks to frameworks like Phonegap, you can wrap it up in a native webview. In iOS 7, the status bar floats above the content. Therefore, everything needs to be pushed 20px (40px Retina-wise) down. This behavior is defined by adding the .native class to the body element.

Besides, another problem to be dealt with are Retina screens. On Retina iPhones, the browser automatically “translates” the 320x480px viewport into the 640x960px screen resolution. While this solution works well for most elements, it fails to reproduce the famous 1px-thick borders iOS uses extensively as they are translated to 2px-thick borders. Intuitively, we would be tempted to set a 0.5px border but this is impossible as it must be a positive integer. Yet, as always, there’s a solution, found by Stephan Bönnemann (see also this link by Brad Birdsall). Basically, it consists of replacing this code: border-bottom:1px solid rgb(200, 199, 204); by this one: box-shadow: 0 1px 1px -1px rgba(200, 199, 204, .5);.

Yet this code doesn’t work with non-Retina screens or iOS 6 (and less). In these cases, there is no solution but to revert to the good old border-bottom:1px solid rgb(200, 199, 204);. In order to detect non-Retina screens, the easiest solution is to use the (-webkit-max-device-pixel-ratio: 1) media query. As for detecting iOS 6 and older version, I think the best solution is to add a custom CSS class (in my sample code, the .ios6 class) with Javascript.

UPDATE (11/24/2013): I just discovered this article by Priit Pirita. This looks like a really good solution too.

 

3.2. Titlebox

The title’s behavior is also extremely finely tuned. Lim Chee Aun described it very accurately in his HackerWeb article, and it doesn’t seem to have changed much in iOS 7.

In order to handle this behavior properly, I had to implement it in Javascript as I don’t see any way to do it in pure CSS. In order to do so, I first loop through all h1 elements as follow:

 var textboxes = document.querySelectorAll("h1"); for (var i=0; i&lt;(textboxes.length-1); i++) TextboxResize(textboxes[i]);

And then, each element goes through the TextboxResize() function.

This function is nothing more than a decomposition of the different cases which may happen:

 

3.3. The navigation bar event

This event aims at roughly recreating the tap on the status bar that makes the content scrolls to top in every native app. Although not perfect, it’s the closest thing we can do to emulate this behavior as there’s no event corresponding to a tap on the status bar. To each h1 element, the TextboxResize() function binds the ScrollTop() function. This one checks whether the content is scrolled or not. If it is, a timer makes the content scrolls back to top fluently.

Nota Bene: If your app is wrapped in a Phonegap container and if you target iOS 7 only, you can remove the navbar’s 20px padding-top (which purpose is to provide a background for the iOS 7’s floating status bar) and replace it by a 20px tall div. Then, you can bind the ScrollTop() function to this div and thus perfectly mimic the behavior of iOS 7 as only a tap on the status bar will make the content scroll top.

 

3.4. The tap event

On touch devices, the tap event is the rough equivalent of the click event on desktop. Yet as Javascript doesn’t support it natively, we still have to use classic onclick declarations. But there’s a problem: iOS automatically adds a 300ms delay between the moment the user taps the screen and the moment when the click event is effectively fired.

This results in a feeling of sluggishness, even if it’s only 300ms. A simple workaround is the use of the Fastclick library, developed by the FT Labs. Once included in the JS files, a simple FastClick.attach(document.body); ensures the this delay no longer exists.

 

Conclusion

These are just a few tricks I found useful to share while I was coding Dedicards. There is still room for improvement. For instance, one drawback is the scrollbar that goes partly behind the navigation bar since the content area scrolls behind it. There’s certainly a workaround, if anyone has an idea, please share it in the comments. Moreover, there is still this navigation bar translucency issue; there might be a clever workaround. If someone has an idea, please share it!

Overall, I’d like to say once again that this is just a proof-of-concept and certainly not an ideal solution. I still think that for heavier applications, native is the only proper solution. Yet if you are a web developer and would like to make a simple app that only handles text, and a few images, this might be appropriate. Similarly, if your app is merely an interface to some web service, I think this could work well.

Your input is of course very welcome or discuss on HN.
If you’re looking for an angular adaptation, Fred Fortier is making one on Github.

Last update: 9/7/2013

 


Comments

Thanks for the article, the way you have broken everything down is remarkable for two reasons: the first is that it shows how simple and straightforward Apple’s design rules really are. The miracle of Apple isn’t that they are the only company with design guidelines, it’s that they are one of the few that really stick to the guidelines they create.

The second thing that made this post remarkable was how simply you were able to replicate the same functionality in likely less code that it would take to do the same thing natively.

I really look forward to the day when HTML matures past desktop UI frameworks, it’s coming slowly but surely and this post demonstrates we’ve passed another milestone recently with HTML5

- Tommy

 

Thanks very much Tommy!

There are still a few glitches, like the scrollbar that goes a bit under the navigation bar… Besides, I think HTML/CSS/JS are still not suitable for proc-heavy apps.
But it’s nice to be able to deploy such app so easily. As you said, HTML5 is getting better and better and that is good news.

- Côme

 

Very nice curated article !
I really want to see a functional cloning of the blur effect with scrollable content underneath, same as you see it in the iOS7 Safari top navigation bar.
I think it can work great for HTML sticky headers.

- Adrian S

 

There are some solutions on the web (see Hacker News thread for some examples).
But none of them can achieve good enough performance to allow scrolling :/

- Côme

 

« As a result, I decided to stick to Lim Chee Aun’s solution. First, I disabled scrolling on the html and body elements. »

please explain how you achieved this.

- quadratini

 

overflow: hidden; ;)

- Côme

 

Instead of the scrollMask div, wouldn’t it be possible to use header:after { … }?

- Jeena

 

Great idea! I’ll look into it.

- Côme

 

Not gunna use github for this but when I downloaded it, the header aligns left with the left link/button in the top bar..

- Tyrant

 

Yeah it does this with Chrome but works well on iPhone.

- Côme

 

Really cool, thanks for sharing.

- 左撇子

 

Nice work!

For those like Adrian S who want blurring with scrolling in front, I put together this using CSS filters a few weeks back

http://www.webdirections.org/blog/creating-ios-7-effects-with-css3-translucency-and-transparency/

- John Allsopp

 

There’s a better way do draw Retina hairlines, using CSS Transforms: http://atirip.com/2013/09/22/yes-we-can-do-fraction-of-a-pixel/

- Priit

 

I’d love to try this but it says it’s not available in my city. Any way I can play with it?

- Nathan

 

@John: Great article! I’ve seen that but I couldn’t implement it fluently for scrolling.
@Priit: Interesting technique!
@Nathan: In the last version, you can add your own picture with the + sign in the top bar ;)

- Côme

 

Hello,

Great article!

I’ve tried to make a similar web-app for IOS 7 and i run i to 1 problem.

I get white status bar text when i run it in as a web-app, not black as id prefer.

Tried out yours and it seams like you have set to black. Do you have any idea how this could be fixed ?

best,
Max

- Max Gru

 

Hello Max,

I think you can find everything you need here: http://bit.ly/HTwSpp
But options are missing, especially to fit iOS 7′s style…
Hope it’s what you’re looking for!

Cheers

- Côme

 

Really good job! I like the simplicity of your code and lack of reliance on external libraries. It seems to work quite well for me on iPhone and I could easily customize to my needs.

- Frederic

 

This project was very useful to me. I appreciate the attention to details and trying to handle every detail correctly. I am using AngularJS so I created this module based on your project: https://github.com/fredfortier/angular-ios7

I am using Bootstrap 3 in the « content » element (but it is not a dependency for this module). It gives me a complete toolkit when combined with your iOS7 pages.

I replaced fastclick by Angular’s equivalent « ngTouch ». I kept your JS and CSS as-is.

It would be great if you could find some time to add an animation to clone the « search » feature of iOS7 (appear in the header like in the Mail app).

- Frederic

 

Hi Frederic,

Well, actually, I think it would be much harder to reproduce the search input experience that fluently but it should be possible somehow.
Roughly, I think you could css-transform the header element to make it disappear towards the top of the screen and then make your input scroll top.
I’m unfortunately unable to spend that time right now. I’m really sorry, if I can, I will take a look at it but I don’t see that happening anytime soon :/

- Côme

 

if you’d putted a 20px tall div on top, you need to put it too:

.scroll {
height: -webkit-calc(100% – 20px);
}

- alexandre

 

Well, not necessarily, because content could scroll behind. But as of today, my favorite solution is to make the header completely opaque.

- Côme

 

How you exported the svg files?

- Fernando

 

Hi,

I found this tutorial great and have downloaded you app as well. It is on the App store at least.

I am trying to use it in my app but it is not working. I am using jquery and jquery mobile, and after a bit of debugging I notice some conflicts between jquery mobile and your js. Do you have any advise on how to solve it? Has anyone used it with jquery mobile?

Thanks in advance for the reply

- daniela

 

@Fernando, I didn’t exported it myself, SunboX (on GitHub) did it.

@Daniela: Well, I don’t use Jquery/Mobile given I find their solution to be rather sluggish on mobile. Could you share some code?

- Côme

 

Hi Côme! I personally think this is an awesome work achieved by using HTML5, CSS, and JavaScript. Congratulations!!!!

- Alfonso

 

Great article Côme!
I would like to apply the solution for my project, a simple app with some text and images and using Phonegap. The only challenge I see is that all views/pages sit in one index.html document, making it easily unmanageable. Do you think it could be adapted to work on different html pages?

Thanks!

- Angela

 

Yes, I think you could use AJAX calls to load stuff dynamically. Just change the Slide() function to make the AJAX calls!

- Côme

 

Hi Côme,

thanks for the reply. I have another question. It seems that the slide in animation suffers from poor performance when loading sections with multiple images. After a bit of research I’ve found that similar problems are occurring on JQ mobile as well when through CSS transform you edit element opacity. They suggest caching images, and using this declaration: webkit-backface-visibility: hidden;
I’ve tried implementing them, but I had no lack.

I’ve downloaded and tried your app and it seems it does not suffer from this issue. Do you have any tips on how to solve it?

Thanks

- Angela

 

So, after a Saturday of tinkering I found out what is causing the performance issue:
.slout .scroll, .slout .scrolMask{
-webkit-animation-name: slout-scroll;
animation-name: slout-scroll;
}

If I comment the code, the animation works smoothly without the blink. I’ve also tried to use hardware accelerator, but with no luck.-webkit-transform: translate3d(0px,0px,0px); -webkit-backface-visibility:hidden;
I guess I can live without changing opacity of the sliding out content, but still I am not convinced and believe to have missed something!

Let me know what yo think.

- Angela

 

Does it support swipe gestures?

- Aero

 

Thanks for this code – really helpful and instructive. Any idea why I don’t seem to be able to swap out the tab bar icons? I’ve tried replacing them with other icons from the Elusive set but I end up getting a blank area where the icon should be.

- John

 

Thanks! Your article really helped with my web app! The site URL may change sometimes, (http://jsbin.com/deyuz/21 or 22 or 23 etc.). You can check it out. Its a small web app, my first game. Im not the best at programing, and the game isn’t that good…

- var name="Someone.";

 

Hi all,

Sorry I haven’t been very quick to answer but here are a few things:
- It doesn’t support touch gestures and I haven’t seen any web app that implements it properly…
- I think the Ionic Framework is incredibly more advanced than what I did, so I’d recommend to use it instead :)
http://ionicframework.com/

- Côme