How to add a skeleton UI to Rails with Turbo

An abstract representation a “skeleton” UI

The introduction of Hotwire in September of 2021, introduced Turbo Frames. A technique that made Internet-veterans think of the stone-age iframe-element. But with a twist.

Turbo Frames can be loaded with HTML from the server. It could be as tiny as just a button (with a different state) or more involved: a list of messages. And it’s for this latter case that having a “skeleton UI” is useful. As a list messages might mean: loading the messages, the sender details and, maybe, fetching the sender’s avatar. That could easily take a bit more time than 200ms.

But first: what’s a skeleton UI? It is a placeholder interface that mimics the basic structure of a screen or component, displayed while content is being loaded, to enhance perceived performance and user experience.

So after the user clicks to load the messages, the skeleton UI is immediately loaded and then replaced with the actual data/components.

This is straight-forward to do with Hotwire’s Turbo Frames. All it is, is some dummy HTML inside the turbo-frame element. The examples given use Tailwind CSS classes.

<turbo-frame id="messages" src="/messages">
  <ul class="flex flex-col p-4 gap-2">
    <li class="flex items-center gap-3 animate-pulse">
      <span class="block w-8 h-8 rounded-full bg-slate-200"></span>

      <span class="block w-1/3 h-4 rounded-sm bg-slate-300"></span>
    </li>

    <li class="flex items-center gap-3 animate-pulse">
      <span class="block w-8 h-8 rounded-full bg-slate-200"></span>

      <span class="block w-1/3 h-4 rounded-sm bg-slate-300"></span>
    </li>

    <li class="flex items-center gap-3 animate-pulse">
      <span class="block w-8 h-8 rounded-full bg-slate-200"></span>

      <span class="block w-1/3 h-4 rounded-sm bg-slate-300"></span>
    </li>
  </ul>
</turbo-frame>

The ul-element within the Turbo Frame is rendered on page-load and once the rendering on the /messages is done it will replace the contents of the messages Turbo Frame.

How to create a skeleton UI

This process is relatively straightforward, yet simply copying and pasting the above HTML snippet into your application is insufficient. For the best user experience, ensure that the skeleton UI closely mirrors the actual UI.

Unfortunately there’s no real easy way to create these. What I usually do is take the actual HTML of one item from the component and turn them into a skeleton UI.

Let’s take an example from one of my SaaS’.

<li>
  <a class="flex items-start px-4 py-2 bg-white gap-2" href="/messages/abc123">
    <span class="inline-flex items-center justify-center shrink-0 uppercase w-5 h-5 mt-0.5 text-xs leading-none font-semibold bg-white border border-gray-200 rounded-full text-gray-500">A</span>

    <section class="w-full grow">
      <p class="flex items-center justify-between">
        <small class="flex items-center text-sm text-gray-600">Alex Taylor</small>

        <span class="flex items-center gap-1">
         <time datetime="2024-02-23T16:43:26.000+07:00" class="hidden text-xs sm:block shrink-0 text-black/40">23-02-2024, 16:43</time>
        </span>
      </p>

      <h5 class="relative flex items-center justify-between text-sm font-semibold tracking-tight">
        <p class="flex items-center text-gray-800 gap-1.5">
          <span class="line-clamp-1">Error message after removing</span>
        </p>
      </h5>

      <p class="text-sm text-gray-500">
        Hello team,

        I hope all is well. I have a little problem testing out further
        possibilities between our setup and the CMS.

        I got an error after the following situation:

        1. I create...
      </p>
    </section>
  </a>
</li>

It’s an message preview, as you see often in messaging- and email apps. Now let’s turn above in something that can be used as a skeleton UI.

<li>
  <a class="flex items-start px-4 py-2 bg-white gap-2" href="/messages/abc123">
    <span class="inline-flex items-center justify-center shrink-0 uppercase w-5 h-5 mt-0.5 text-xs leading-none font-semibold bg-gray-200 border border-gray-200 rounded-full text-gray-500"></span>

    <section class="flex flex-col w-full gap-1 grow animate-pulse">
      <p class="flex items-center justify-between">
        <small class="flex items-center w-1/4 h-4 text-sm bg-gray-600"></small>

        <span class="flex items-center gap-1">
          <time class="hidden sm:block text-xs bg-black/40 h-3 w-[100px]"></time>
        </span>
      </p>

      <h5 class="relative flex items-center justify-between text-sm font-semibold tracking-tight">
        <p class="flex items-center h-4 w-1/3 bg-gray-800 gap-1.5">
          <span class="line-clamp-1"></span>
        </p>
      </h5>

      <p class="w-1/2 h-4 text-sm bg-gray-500">
      </p>
    </section>
  </a>
</li>

You notice all HTML element are still in place, but I’ve replaced and added a few classes:

  • removed all text;
  • replaced text-gray-* with bg-gray-*;
  • added both h-* and w-* classes to.

Some alignment and spacing are needed to make sure the “switch” is as smooth as possible, meaning no elements should move around.

I often also create a separate ViewComponent for this. If the message component would be MessageComponent the skeleton UI would be Message::SkeletonComponent. Meaning the file would live in app/components/message/skeleton_component.rb. Keeping things tidy and organized.

It’s a bit of work, but that’s how you get a smooth user experience with a skeleton UI in your Rails app using Turbo Frames.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built using ViewComponent

  • Designed with Tailwind CSS

  • Enhanced with Hotwire