Lessons Learned In Big App Development, A Hawaiian Airlines Case Study

About The Author

Cory is a UI designer and developer with over 15 years of experience improving the web. He runs a boutique agency in Boulder, CO called User Kind, and has … More about Cory ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

Today, join Cory Shaw while he reflects on some of the mistakes he and his team made, the tools they used, the workflows and guidelines they followed, and even some of the custom tools they built while working on the new Hawaiian Airlines website. All while growing a UI development team from one to over ten people to get the job done. It was a rollercoaster ride like no other, but they have prevailed and built what he believes to be one of the best airline-booking experiences on the web. This article and the information herein has been shared with the explicit permission and generosity of Hawaiian Airlines.

Having spent over two years making it, we just pressed the “Ship” button on the new Hawaiian Airlines website. It has been the biggest project of my career, and I’ve worked with the most talented team I’ve ever worked with. Everything was rebuilt from the ground up: hardware, features, back-end APIs, front end, and UX and design. It was a rollercoaster ride like no other, but we have prevailed and built what I believe to be one of the best airline-booking experiences on the web. Yes, humble, I know!

Join me while I reflect on some of the mistakes we made, the tools we used, the workflows and guidelines we followed, and even some of the custom tools we built, all while growing a UI development team from one (yours truly) to over ten people to get the job done.

Full disclosure: Our company, User Kind, is a vendor for Hawaiian Airlines, and all opinions expressed here are my own. This article and the information herein has been shared with the explicit permission and generosity of Hawaiian Airlines.

Humble Beginnings

When I came aboard this project as a UI developer, Hawaiian Airlines had already hired another agency to rethink the UX and design of the existing 10-year-old website. That agency delivered a 500+ pages wireframe document, a handful of beautiful annotated Photoshop mockups and a front-end style guide. Seeing these deliverables immediately got me excited about the project and some of the fun UI development challenges that lay ahead.

Flight Hop

Flight Hop
The “hops” between stops are dynamic and represent the duration of that flight relative to the total flight time. Seemed like a fun thing to build using SVG or canvas. (View large version)

Travel Goals

Travel Goals
When I saw this, I envisioned the orange progress bar and plane icon animating across the screen, while the “miles to go” number counted down. (View large version)

Price Chart

Price Chart
The price chart calculates a relative height for each day, based on the data at hand, fast-forwarding through months while making AJAX calls, and readjusting bar heights based on new data. (View large version)

The Front-End Sandbox

Around the time I was getting started, a large back-end team of 40 or so developers were ramping up on rebuilding all of their service APIs. Knowing that there was a tsunami of UI work to do, no back-end APIs for the front end to consume yet, and a hard deadline staked in the ground, we got to work.

Because the back-end stack was still being defined and built behind a private network, we started with a lightweight front-end sandbox to begin building UI components.

Here’s what the stack of tools and workflow looked like:

Sandbox Dev Stack
Stack of tools and workflow. (View large version)

Dynamic Templates Fed by Static Data

While working in the sandbox environment, we used AngularJS to create dynamic templates based on a static JSON, which would eventually be replaced with live endpoints once we delivered the code. Sometimes the back-end folks would send us a JSON file generated from real flight data, and other times we would just define it ourselves if the data didn’t exist yet.

Using static JSON data worked OK for a while, but once we started building some of the more complex UI components, we quickly ran into a problem: multiple data states.

Take flight results, for example. You have one-way, round-trip and multi-city flight results, each with up to four layovers, overnight flights and multiple airlines. You can even travel back in time if you fly across the right time zones at the right time!

Given the thousand-line JSON files, hand-tweaking the JSON to test other states was a chore and prone to human error.

We needed a better way to build and test all of these different states in the sandbox. So, Nathan set out to solve this problem and came up with what we call the “data inspector”:

Data Inspector
We used the data inspector to simulate live data in the static sandbox environment. (View large version)
Data Inspector Stack
The data inspector used PouchDB to handle localStorage, which also allowed us to sync up to a lightweight CouchDB server so that we could share data states between team members. (View large version)

Armed with the data inspector, we were able to prepare the front-end code so that it was production-ready when we delivered it to be hooked up to live data. As a bonus, designers and product owners could use this tool on the demo Heroku website to ensure that everything looked as intended across states.

Tossing Code Over The Fence

Spoiler alert: Don’t ever do this!

When it came time to integrate the front-end code with back-end services, we had to toss it over the fence to the folks who were integrating it in a totally different environment (.NET) with completely different tools (Visual Studio and Team Foundation Server), tucked securely behind a private network in Hawaii.

While this worked OK initially, it quickly became a nightmare. Product folks would request changes to the UI; we would make those changes in the sandbox, then toss it back over. Code changes would then have to be hand-merged because we had Git on one side and Team Foundation Server on the other. With different file and folder structures, these two repositories didn’t play nice together.

We quickly put an end to this and worked with the IT team to get access into the walled paradise. However, this process set us back months of productivity, as we switched to a completely different development stack, got VPN access, learned a different toolset and set up our virtual machines to match what the back-end team was using.

From then on, we’ve worked directly with the back-end teams to build and integrate the UI code, using the scrum process in two-week sprints, and things have gone a lot smoother since.

In the short term, the sandbox gave us a huge head start. We got to use a bunch of modern tools and workflows that we were all familiar with. It made us really efficient. Given the circumstances, it might have been the right move, but we waited way too long to rip off the bandage and hop over the fence once it was ready.

Sandbox Learnings

  • If you’re using Git, choose a branching model carefully on day one, and ensure that it fits your team, project and workflow.
  • If your Git branching strategy is done right, then reverting or cherry-picking features over the timeline of your project should be a cheap and easy task.
  • If building the front end of an app with real data and endpoints isn’t possible, then figure out a way to make it possible. (Mocked-up endpoints would have been better.)
  • Avoid multiple teams working across multiple environments at all costs, even if it causes delays up front.
  • Establish your tools, workflows and environment early on, and ensure that everyone on the team uses them.
  • Had we taken a more forward-thinking approach, it would have given us a big leg up in the long run, and we would’ve avoided the mid-project slump altogether.


In the beginning of this project, we adopted the methodology that keeping the HTML light, with very few CSS classes, while using LESS’ :extend heavily was the way to go.

It’s nice because when your design changes in the future, your HTML won’t be full of a lot of CSS classes, and you shouldn’t have to touch it. Simply update your LESS styles, and change your :extends.

Most elements in the HTML had either no class or a single defining class:

<section class="my-section">
   <p>Some Text</p>

Then, in our LESS, we’d have styles like this:

.my-section {

The net result of this method is a lot of selectors in the CSS output. After a year of coding, our CSS output got unwieldy, with thousands of lines of this:

.ha-modal .help-template h2,
.ha-modal .help-template h3,
.ha-modal .help-template h3:first-child,
.ha-help.collapsable-block h4,
.tooltip-block h4,
.traveler-lg .name,
address h4,
.ha-cms-teaser-sidebar .heading,
[ha-calendar] .ha-calendar-month,
.ha-modal#locationModal .destinations-container .standard-location .heading,
[ha-alert] .alert .alert-content .alert-content-primary,
[ha-avatar] .avatar .name,
[ha-avatar] .avatar.small .name,
[ha-tooltip] .ha-tooltip h4,
[ha-global-alert] .global-alert .alert-content .alert-content-primary,
[ha-promo-tile-other-small] .promo-tile.tile-small .headline,
[ha-promo-tile-other-large] .promo-tile .headline,
[ha-child-nav-tile] .child-nav-tile .page-title,
.navtray-content-inner--stackedlistwrap .stackedlist-li-title,
.lte-ie7 .navtray-content-inner--stackedlistwrap .stackedlist-li-title,
.ha-flight-hop .departure-city,
.ha-flight-hop .arrival-city,
.ha-receipt .trip,
.ha-my-trip-itinerary .trip-header span.segment-city,
.ha-my-trip-itinerary .segment .check-in .status,
.ha-my-trip-itinerary .segment .check-in .status:before,
.ha-my-trip-itinerary .segment .check-in .status.green:before,
.ha-my-trip-itinerary .segment .check-in .status.red:before,
.ha-my-trip-itinerary .segment .check-in .status.yellow:before,
.ha-flight-status .flight-info .flight-number,
.ha-flight-status .flight-info .flight-route,
.ha-print-confirmation .reservation-code-title,
.ha-my-trips-itinerary-details .trip-header span.segment-city,
.ha-my-trips-eticket-receipt .trip-header span.segment-city,
.ha-my-trips-itinerary-details .segment .segment-header .col,
.ha-my-trips-eticket-receipt .segment .segment-header .col,
.ha-my-trips-itinerary-details .segment .leg .leg-details .status,
.ha-my-trips-eticket-receipt .segment .leg .leg-details .status,
.ha-my-trips-itinerary-details .segment .leg .leg-details .status:before,
.ha-my-trips-eticket-receipt .segment .leg .leg-details .status:before,
.ha-my-trips-itinerary-details .left-heading .trip-locations,
.ha-my-trips-eticket-receipt .left-heading .trip-locations,
.ha-book-flight-results .segment .selected-flight-info,
.select-class-wrapper a,
.ha-book-flight-results .discount-applied .credit-applied {
  font-style: normal;
  font-size: 0.9375em;
  font-family: "helvetica-neue", "HelveticaNeueLT Std", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: bold;
  text-transform: none;
  line-height: 1.4;
  letter-spacing: 0.02em;

Fun fact: Did you know that Internet Explorer 9 and below will stop processing a given CSS file once it reaches 4095 selectors? Heavy use of :extend put us way over that limit early on. Figuring out why the website looked totally messed up in Internet Explorer 8 and 9 took a bit of debugging and research. We ended up using a Gulp task to break up the CSS files for old versions of the browser.

This ended up being really bad. It bloated our CSS with an output that made it difficult to debug styles in the inspector.

Mixins Vs. Extend

When our CSS output started exceeding 100 KB in size, a question arose. What would output a smaller style sheet: more styles (using @mixin) or more selectors (using :extend)?.

I’ll let Jake explain:

“After testing it out, we discovered that, despite :extend outputting significantly less CSS, Gzip compression of the redundant mixin styles could actually translate into a similar if not smaller file size. What puts this idea over the top is that transitioning to mixins would make the DOM inspector CSS much more legible. We would no longer have 200 unrelated selectors grayed out for that h1 you’re trying to debug (which can make the inspector lag and reduce legibility). We did a small Gzip test comparing a small-scale mixin-ed style sheet versus an :extend-ed style sheet, and the mixin version actually came out on top.”

So, we did a big overhaul to change all :extends to @mixins. (We covered 80% with a simple script, the rest by hand.)

Thus, this…

.my-section {

… became this:

.my-section {
   h1 {.header-uppercase-1}
   p {.bodycopy-sans-3}

This discovery was an improvement, but the bloated CSS could have been avoided altogether if we had adopted an entirely different framework…


Looking back on all of this, our CSS would have reduced in size and our development productivity would have increased if we had established a pattern with more defining classes in the markup (OOCSS and/or BEM).

Here are the pros of OOCSS and BEM:

  • Style sheets are smaller, flatter and easier to maintain.
  • Troubleshooting and development of styles are more efficient:
    • Source maps can tell you where to find the source LESS code.
    • Modifying styles in the browser (for experimenting) is easier because they will appear as different syles.
    • The DOM will tell you what the custom class is versus what the global classes are.
    • You can more easily break out specific style sheets to serve only what a page or section needs (rather than a lot of classes being downloaded that the page doesn’t refer to).

And here are the cons of OOCSS and BEM:

  • The HTML is more wieldy, with a lot of CSS classes.
  • You’ll have less flexibility to make CSS-only changes down the road.
  • When the design changes, you’ll likely need to modify the HTML classes.

In hindsight, OOCSS and BEM clearly would have been ideal frameworks to approach a project of this size.

CSS Learnings

  • Agree on a general approach across the team, or adopt an OOCSS-esque approach, like BEM.
  • Use a linter like Jacob Gable’s LESS Lint Grunt plugin to keep your LESS and CSS inline with your patterns.
  • Stay away from using :extends as much as possible on a big project. The way it works is smart, but the output is confusing and difficult to debug.
  • Use classes that are flat and reusable throughout the project, and continually analyze existing classes when creating new ones.


When I came aboard this project, I had a lot of experience with jQuery, jQuery Mobile and vanilla JavaScript, but I hadn’t touched AngularJS or similar JavaScript frameworks. The paradigm shift to AngularJS was a struggle for me at first; but, as many others have experienced, once I got over the learning curve, I fell in love.

Custom UI Components

What makes AngularJS a great solution for a big project like the Hawaiian Airlines website is the amount of flexibility it gives you to create custom UI components.

All of that flexibility means that there are a lot of ways to skin the AngularJS cat. In the beginning, we skinned it in ways that made our code difficult to test and difficult to reuse in different contexts. We’d have a directive that depended on some parent scope variable, and when that didn’t exist, the directive would break. We learned pretty quickly that if you don’t have an isolate scope in your directive, you’re asking for trouble.

Over the course of the project, we learned to think about AngularJS directives more as self-contained web components with an API.

AngularJS directives should be very self-centered. They shouldn’t know or care about the world they live in, so long as their basic needs are met, as defined by an API in the form of element attributes:

   excluded-airport-codes="['OGG', 'DEN']"

In the example above, the data you feed this directive via the attributes tells it how to behave and exposes a way to pull data back out of it, yet completely isolates its inner workings and template that renders to the DOM.

AngularJS Performance

While AngularJS magically data-binds everything defined on $scope two ways, this magic doesn’t come for free. For every item on $scope, a listener is created that detects changes to it. When changes are detected, it goes through and updates everywhere else it is used. Each time AngularJS loops through all of the items on $scope, we call that a digest cycle. The more stuff you have attached to $scope, the harder it has to work and the slower your digest cycle becomes.

In a big application such as Hawaiian Airline’s flight results, we started noticing laggy performance on tablets and slow desktop computers. Upon investigation, we realized that the page had over 5,000 watchers, and the digest cycle was taking several hundred milliseconds!

With a new problem and awareness of AngularJS’ performance, Nathan and Scott set out and built a handy tool to monitor AngularJS performance, and they open-sourced it.

This tool ended up being key in troubleshooting and taming AngularJS performance across the website. Check it out: You can see AngularJS performance data on the live website by adding ?performance=true to any page’s URL.

AngularJS Performance Panel
The performance panel gave us valuable insight into the number of watchers and overall performance of a given page. (View large version)

In conjunction with the performance tool, we used AngularJS’ bind-once directive to ensure that we have watchers only on data that needed to change.

As a result, we brought our watchers from over 5,000 down to under 500, and we saw a nice bump in responsiveness on tablets and slow devices.

AngularJS Learnings

  • With great power comes great responsibility. Make sure you understand the inner workings of your chosen framework so that you harness it for good and not evil.
  • AngularJS has taught me an entirely different way to think about constructing a UI, such as breaking components down to their bare reusable essence, and avoiding DOM manipulation via jQuery altogether.
  • Think of directives as web components that expose an API into them, and keep your scope isolated from the outside world to avoid bugs and headaches.

Custom Form Controls

Booking travel online basically consists of a complex set of forms. So, designing beautiful custom form controls seemed obvious, and everyone (me included) was excited about it.

Looking back, if I had to pick the single most painful thing we did on this project, it would be the custom form controls.

You might not realize it, but those form controls that come out of the box in your browser do a lot of heavy lifting:

  • They ensure that people with accessibility challenges can still use them.
  • They keep track of focus, blur, active, inactive states.
  • They allow the user to cycle through all fields using the “Tab” key.
  • They figure out how and where to place dropdown menus based on the page’s scroll position.
  • They allow the user to type up to several letters to jump to an item in a dropdown menu.
  • They auto-scroll dropdown menu items for long lists.

When we decided to roll our own form controls, we took on the burden of reinventing the wheel and supporting all of the requirements above.

We ended up with a solution that uses AngularJS to hide the native HTML of select dropdowns, checkboxes and radio buttons, and replaces them with alternate markup for which we had full control over styling.

Old Form Controls
This is what our custom form controls looked like. (View large version)

While this approach gave us OCD-level control over every pixel, it ended up causing all kinds of obscure bugs and accessibility issues in complex situations, which we spent countless hours patching.

In the end, we decided to scrap these custom form controls in favor of their native counterparts. We realized that, while we couldn’t achieve the pixel perfection of a pure custom solution, we could get 99% of the way there just by using background images and pseudo-selectors on the native input HTML. In the case of