UI Skeletons, Ghost Elements, Shell Elements? They are all the same, and I bet you probably heard any of these concepts before. If you didn’t I’m sure you have seen this pattern in many of the apps you use daily.

Ionic Skeleton Tutorial
Ionic Skeleton Tutorial
Ionic Skeleton Tutorial

Think of Shell Elements as cool content placeholders that are shown where the content will eventually be once it becomes available.

In this post, I will show you the importance of adopting the App Shell pattern in your Ionic apps and discuss how to implement it using Ionic Angular and some advanced CSS techniques.

In this guide, you will note that we will refer with different names to the same pattern: Skeleton Screens, App Shell, Ghost Animations. They are all the same, and all those names are valid.

This post is part of the "Mastering Ionic Framework" series which deep dives into Ionic more advanced stuff. Don’t be afraid, if you are new to Ionic Framework, I strongly recommend you to first read our getting started with Ionic tutorial.

At IonicThemes we are big fans of Learning by example, that’s why all our Ionic tutorials include complete and free code examples for you to reuse in your Ionic projects. We strive to create the best content for Ionic Framework, both tutorials and templates, to help the Ionic community succeed.

You can download all the source code of this ionic tutorial by clicking the GET THE CODE button from above. Also, we published an online demo of what we are going to build in this ionic skeleton and animations guide!

We can help you create better apps with our detailed and professional starters, crafted with love and dedication. Please check them out and let us know if you have any questions or feedback.

The following skeleton animations are part of our latest Ionic 6 Full Starter App. It’s an ionic starter that you can use to jump start your app development and save yourself hundreds of hours of design and development.

Ionic skeleton starter
Ionic skeleton starter
Ionic skeleton starter

When building an app, you need to make sure that you are doing everything correctly from a technical perspective to ensure great performance. But another, often ignored, and highly important part of performance is called: perceived performance.

Let me explain the differences between Actual web Performance and User Perceived Performance.

Actual Web Performance

Web performance includes a bunch of technical approaches and techniques such as:

  • Reducing bundle size
  • Reducing server response time
  • Reducing http requests

Those are all about optimizing the medium (bandwidth, requests, etc) and resources (images, fonts, files, etc) we develop with. They will help you achieve a website that loads at lighting speed.

But that’s not enough, you want your site to feel fast besides being actually fast.

Examining network timelines and PageSpeed scores is all well and good, but there’s a whole area of performance optimization that this technical stuff doesn’t cover.

Perceived Performance is a measure of how quick a user thinks your site is, and that’s often more important than it’s true speed.

Picture the perception of time as a continuum of different events, if users don’t see any new events regularly then they may perceive slowness. The trick to get around this is to create a mix of skeleton loading screens and animations that occupy their time.

Strive for getting meaningful content to your humans as quickly as possible. Then, once they have something they can read or interact with, add more content on top. Throw in the rest of the styles, fetch the JavaScript needed to obtain a fancier experience, but always provide them with something to do other than stare at a motionless screen!

If you want to keep learning about user perceived performance, this post about user perception, and this one about our brain quirks of perception were very useful to me.

The UX of waiting times

Let’s face it, most of the time we can’t avoid having to wait for information. But we can make the wait feel shorter. By showing page elements incrementally and giving some indication of what’s going on, the user feels more comfortable.

If we contrast this with the scenario of waiting until everything is loaded, the usability and user perceived performance improvements are stupendous.

ionic user perceived performance
ionic user perceived performance

The Alternatives

This is not something completely new, developers have been using spinners and progress bars ever since. But those approaches fall short, we must do better.

ionic loading component

Notice how the page with the app skeleton elements has several advantages that translate to a better UX. We don’t block the user from interacting with the app. It’s a continuous flow from the page transition, the animation while the content is being loaded, and finally when all the content is available.

This progressive, non blocking flow from one state to the another makes the app feel faster and smoother without flashes and shocking transitions between loaders to fully loaded pages.

Fortunately this cutting edge, advanced technique can be easily implemented with Angular and Ionic.

An App Shell Architecture is useful for getting some initial HTML to the screen fast, typically the skeleton of your UI, but likely does not contain any real data.

In my opinion, a proper App Shell implementation consist of two principles:

  • Do not block page transitions
  • Show content placeholders while loading data for that page

To achieve these goals we are gonna combine Angular Route Resolvers with some custom elements and some advanced CSS techniques.

Angular Route Resolvers

New to Angular Route Resolvers?

Angular Route Resolves are a special kind of route guards. They enable us to pre-fetch data from the server before navigating to a route. This way, the data is ready the moment the route is activated.

Routers, can go from being super easy to complex quite quickly. However, do not fear: we have put together a great guide going over routing in an Ionic/Angular app with some helpful tips.

By default, Angular Route Resolvers won't transition to the page until the Resolver Observable completes.

Let's suppose the backend is slow and takes 5 seconds to fetch data and return it to the client. The expected behavior for that scenario is that the page transition will be blocked for 5 seconds until the server sends data back to the client.

ionic blocked transition

A minimal improvement would be to show a ionic loading spinner while the Resolver Observable completes.

ionic blocked transition

Non blocking Resolvers

To avoid waiting for the Observable to complete, we can wrap the base Observable (the one we are getting data from) with a dummy Observable, Subject or Promise that emits the base Observable and immediately completes.

Resolver using a ReplySubject that emits the base Observable and then completes:

resolve() {
  // Resolver using a ReplySubject that emits the base Observable and then completes
  const subject = new ReplaySubject();
  subject.next(baseObservable);
  subject.complete();
  return subject;
}

Resolver using an Observable that emits the base Observable and then completes:

resolve() {
  // Resolver using an Observable that emits the base Observable and then completes
  const observable = Observable.create((observer) => {
    observer.next(baseObservable);
    observer.complete();
  });
  return observable;
}

Resolver using a Promise that resolves the base Observable:

resolve() {
  // Resolver using a Promise that resolves the base Observable
  const promise = new Promise((resolve, reject) => {
    resolve(baseObservable);
  });
  return promise;
}

I like the Promise approach as it’s more straightforward, you end up resolving an instant promise for the base Observable.

ionic nonblocking resolvers

To complete this Ionic router resolvers implementation, it would be handy to have a solution that enable us to resolve a shell model while we wait, and then the real data when it’s available from the backend. This solution should also allow our stream of data to be cached and pushed.

We won’t cover the details of this mechanism in this Ionic tutorial as we have already done so in previous posts. To learn more about app routing and navigation using Ionic and Angular, follow this detailed routing tutorial.

Remember you can use the Ionic demo app we built for this tutorial to try these examples by yourself. Also you can download all the source code of this ionic app by clicking the Get the code button from the beginning of the page.

Now that we found a non-blocking approach to use in our Angular Route Resolvers for our Ionic Framework apps, we need to find a solution to present and transition between the skeleton pages (loading state) and the page layout (loaded state).

There are two types of shell elements: Shell Overlays (use different components to render either the shell DOM and real DOM) and Inline Shells (reuse the same DOM elements to show either shell or real DOM with business data).

Option 1: Shell Overlays

This is a straightforward approach. We define both skeleton and view layouts and switch/animate the transition between them when page data is available.

<ng-container *ngIf="!routeResolveData">
  <!-- Shell layout here -->
  </ng-container>
  <ng-container *ngIf="routeResolveData">
  <!-- View layout here -->
</ng-container>

Pros:

  • Provides separation of concerns. Real DOM is neither aware of nor impacted by shell DOM
  • It’s easier to animate a shell layout that’s decoupled from the main view layout.

Cons:

  • We end up having many duplicate code, both for the layout and styles. This makes our app harder to maintain.

Option 2: Inline Shells

This type of shells use the same layout (DOM elements) to present the loading state using the shell model and the real data once it’s available.

The idea behind these skeleton elements is to show a loading state when the element is binded to an empty/null object and then progressively transition the loading state once the binded object has the real data. It’s almost a CSS only solution.

By combining this technique with non blocking Angular Resolvers, we will be able to immediately present a shell model while fetching the real data asynchronously. Finally when real data is available, we just need to animate the transition between the loading screens and loaded states.

<ion-row>
  <ion-col size="4">
    <app-image-shell class="add-spinner" [src]="routeResolveData?.image" [alt]="'Sample Image'"></app-image-shell>
  </ion-col>
  <ion-col size="8">
    <h3>
      <app-text-shell [data]="routeResolveData?.title"></app-text-shell>
    </h3>
    <p>
      <app-text-shell lines="3" [data]="routeResolveData?.description"></app-text-shell>
    </p>
  </ion-col>
</ion-row>

Pros:

  • It’s much easier to mold and style the shell representation of our view data as they share the same layout.
  • Using this type of shells introduce minimal friction to the view layout as it doesn’t add meaningless, redundant, specific skeleton ui layouts.

Cons:

  • As they share the same layout, depending on the use case, there may be challenges to animate transitions between real vs shell renderings.

If you need help adding the Skeleton screens and login to your Ionic app, I suggest you to take a look at Ionic 5 Starter App - PRO. It will save you LOTS of development and design time. It has different types of customizable skeleton layouts that you can reuse in your apps.

Ionic Skeleton Screens
Ionic Skeleton Screens
Ionic Skeleton Screens

On the following video you can see how easy is to use the Shell Components from this template in your own Ionic Framework project.

After analyzing multiple use cases, I realized most UIs can be deconstructed into two data bindable element primitives: text and images.

Let’s see how easily we can implement both image-shell and text-shell components with Ionic Framework to build UIs that progressively translate from the ghost loading state to the final state displaying real data.

Image Shell

This is a simple Ionic component that will enable us to load images with an elegant skeleton loading layout.

import { Component, Input, HostBinding } from '@angular/core';

@Component({
  selector: 'app-image-shell',
  templateUrl: './image-shell.component.html',
  styleUrls: [
    './image-shell.component.scss'
  ]
})
export class ImageShellComponent {
  _src = '';
  _alt = '';

  @HostBinding('class.img-loaded') imageLoaded = false;

  @Input()
  set src(val: string) {
    this._src = (val !== undefined && val !== null) ? val : '';
  }

  @Input()
  set alt(val: string) {
    this._alt = (val !== undefined && val !== null) ? val : '';
  }

  constructor() {}

  _imageLoaded() {
    this.imageLoaded = true;
  }
}
<ion-spinner class="spinner"></ion-spinner>
<img class="inner-img" [src]="_src" [alt]="_alt" (load)="_imageLoaded()"/>

It basically works by showing a loading indicator while fetching an image source. By listening to the (load) event attached to the <img/> element, once the image has loaded, we hide the spinner.

Text Shell

This ionic component works by wrapping the text node with a skeleton loading indicator while fetching data.

While there are empty values the component adds some loading styles and animations. Whereas when there are non-empty values, the loading state is removed.

import { Component, Input, HostBinding } from '@angular/core';

@Component({
  selector: 'app-text-shell',
  templateUrl: './text-shell.component.html',
  styleUrls: [
    './text-shell.component.scss'
  ]
})
export class TextShellComponent {
  _data: '';

  @HostBinding('class.text-loaded') textLoaded = false;

  @Input() set data(val: any) {
    this._data = (val !== undefined && val !== null) ? val : '';

    if (this._data && this._data !== '') {
      this.textLoaded = true;
    } else {
      this.textLoaded = false;
    }
  }

  constructor() { }
}
<ng-container>{{ _data }}</ng-container>

There’s no rocket science here, if the value is not empty, we add a text-loaded class and some fancy CSS to style and animate accordingly.

This component can be used alone or wrapped with a text element (h1, h2, h3, p, span, etc).

<h1>
  <app-text-shell [data]=""></app-text-shell>
</h1>

<h2>
  <app-text-shell [data]=""></app-text-shell>
</h2>

<h3>
  <app-text-shell [data]=""></app-text-shell>
</h3>

<h4>
  <app-text-shell [data]=""></app-text-shell>
</h4>

<h5>
  <app-text-shell [data]=""></app-text-shell>
</h5>

<p>
  <app-text-shell [data]=""></app-text-shell>
</p>
ionic text skeleton

Adding skeleton animations with some nifty CSS tricks

I have to admit that I’m really into micro interactions. That’s why I invested quite some time exploring different animation approaches for the text-shell component.

From the beginning I challenge myself to use just CSS and avoid adding new DOM elements to the mix (that would be much easier but will eventually end up producing a bloated layout full of meaningless DOM elements).

Background Gradient animation

Inspired by how the Facebook content placeholder works, I figured out I could achieve the same result without the many tiny DOM element masks.

app skeleton animations

This ghost animation works by setting a background gradient beneath some mask elements.

Facebook’s approach

This is how the facebook skeleton content placeholder layout looks like.

<div class="animated-background">
  <div class="background-masker header-top"></div>
  <div class="background-masker header-left"></div>
  <div class="background-masker header-right"></div>
  <div class="background-masker header-bottom"></div>
  <div class="background-masker subheader-left"></div>
  <div class="background-masker subheader-right"></div>
  <div class="background-masker subheader-bottom"></div>
  <div class="background-masker content-top"></div>
  <div class="background-masker content-first-end"></div>
  <div class="background-masker content-second-line"></div>
  <div class="background-masker content-second-end"></div>
  <div class="background-masker content-third-line"></div>
  <div class="background-masker content-third-end"></div>
</div>

They add a full background animation using a gradient.

ionic app shell

And on top of that background animation, they add white masks.

ionic skeleton animations
Our approach

The solution I came out with, uses multiple linear-gradients to compose the background-image property of the element. By adjusting the position and size using multiple background-position and background-size values, we achieve the same result as the facebook skeleton loading styling but using just CSS.

// Two lines text shell
.text-shell {
  background-repeat: no-repeat;
  background-image:
    /* First line: 95% width grey, 5% white mask */
    linear-gradient(to right, #CCC 95% , #FFF 95%),
    /* Separation between lines (a full width white line mask) */
    linear-gradient(to right, #FFF 100%, #FFF 100%),
    /* Second line: 65% width grey, 35% white mask */
    linear-gradient(to right, #CCC 65% , #FFF 65%);

  background-size:
    /* First line: 100% width, 16px height */
    100% 16px,
    /* Separation between lines: a full width, 3px height line */
    100% 3px,
    /* Second line: 100% width, 16px height */
    100% 16px;

  background-position:
    /* First line: begins at left: 0, top: 0 */
    0 0px,
    /* Separation between lines: begins at left: 0, top: 16px (right below the first line) */
    0 16px,
    /* Second line: begins at left: 0, top: (16px + 3px) (right below the separation between lines) */
    0 19px;
}

Note: I found this post on adding many gradients to the background-image property very useful. Also I created this sass code to help me out while I was wrapping this multi gradient technique into a Sass mixin.

We can go a step further and add an animation to the background just like the Facebook loading animation example.

To deal with the background animation, we used two pseudo elements ::before to handle the animation that goes beneath the masks and ::after to handle the multi-gradient background containing the masks.

.gradient-animation {
  &::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background-repeat: no-repeat;
    background-image:
      /* First line: 95% width grey, 5% white mask */
      linear-gradient(to right, transparent 95%, #FFF 95%),
      /* Separation between lines (a full width white line mask) */
      linear-gradient(to right, #FFF 100%, #FFF 100%),
      /* Second line: 65% width grey, 35% white mask */
      linear-gradient(to right, transparent 65%, #FFF 65%);

    background-size:
      /* First line: 100% width, 16px height */
      100% 16px,
      /* Separation between lines: a full width, 3px height line */
      100% 3px,
      /* Second line: 100% width, 16px height */
      100% 16px;

    background-position:
      /* First line: begins at left: 0, top: 0 */
      0 0px,
      /* Separation between lines: begins at left: 0, top: 16px (right below the first line) */
      0 16px,
      /* Second line: begins at left: 0, top: (16px + 3px) (right below the separation between lines) */
      0 19px;
  }

  // The animation that goes beneath the masks
  &::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background:
      linear-gradient(to right, #EEE 8%, #DDD 18%, #EEE 33%);
    background-size: 800px 104px;
    animation: animateBackground 2s ease-in-out infinite;
  }
}

Note: This solution doesn’t play well if you require the text-shell to have a transparent background as the masks need a solid color to work properly.

Bouncing Lines Background animation

As we mentioned above, the issue with the previous approach is that it depends on a solid mask color and that doesn’t play well if your use case requires transparent backgrounds.

That got me into thinking alternative ways to tackle the ghost animation.

This animation works by animating the background-size property to achieve a bouncing effect.

.bouncing-animation {
  &::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background-repeat: no-repeat;
    background-image:
      /* First line: 95% width grey */
      linear-gradient(to right, #EEE 95%, transparent 95%),
      /* Separation between lines (a full width transparent line mask) */
      linear-gradient(to right, transparent 100%, transparent 100%),
      /* Second line: 65% width grey */
      linear-gradient(to right, #EEE 65%, transparent 65%);

    background-size:
      /* First line: 100% width, 16px height */
      100% 16px,
      /* Separation between lines: a full width, 3px height line */
      100% 3px,
      /* Second line: 100% width, 16px height */
      100% 16px;

    background-position:
      /* First line: begins at left: 0, top: 0 */
      0 0px,
      /* Separation between lines: begins at left: 0, top: 16px (right below the first line) */
      0 16px,
      /* Second line: begins at left: 0, top: (16px + 3px) (right below the separation between lines) */
      0 19px;

    animation-direction: alternate-reverse;
    animation-name: animateMultiLine;
    animation-fill-mode: forwards;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
    animation-duration: 1s;

    @keyframes animateMultiLine {
      0%{
        background-size:
          /* First line animation initial state: 80% width, 16px height */
          80% 16px,
          /* Separation between lines: a full width, 3px height line */
          100% 3px,
          /* Second line animation initial state: 60% width, 16px height */
          60% 16px;
      }

      100%{
        background-size:
          /* First line animation final state: 100% width, 16px height */
          100% 16px,
          /* Separation between lines: a full width, 3px height line */
          100% 3px,
          /* Second line animation final state: 100% width, 16px height */
          100% 16px;
      }
    }
  }
}

Note: As we don’t use masks, this approach works well with use cases that require transparent backgrounds.

Note: The approaches, Sass mixins, and implementations we followed in the previous examples were the very first versions of the text and image shell components.

We have evolved and polished those components to a production ready state in our most recent Ionic Framework Template. Check out the template’s live preview to see the shell solution we recommend for production apps.

As we discussed in this Ionic tutorial, perceived performance is commonly overseen, but these tiny bits of usability can drastically improve your app’s performance, usability, and user experience.

Have a look at the Ionic demo app we put together which wraps all the examples and techniques we covered in this skeleton loading Ionic tutorial.


Ionic brings a lot of new possibilities and improvements, and at IonicThemes we want to help you getting the most out of this powerful framework. We are constantly creating new tutorials and guides about ionic features so if you want to learn about a specific topic please leave a comment below with your suggestions.

As you’ve seen in this Ionic app shell tutorial, creating shell or skeleton screens with Ionic and Angular is straightforward. What are you waiting for? Add app shell screens to your Ionic application today!