Skip to content

Feature: Loading States and Skeleton UI #92

Description

@AgentKush

Summary

When navigating to the Mods or Tools pages, there's a noticeable delay while data is fetched from Google Cloud Firestore. During this time, the user sees either a blank page or a flash of empty content before the data appears. Adding skeleton loading states and a styled Turbo progress bar would give users immediate visual feedback that content is loading.

Related: This revisits closed issue #24 ("Create a loading screen to show while we fetch data from firestore") with a more modern approach using skeleton UI instead of a generic loading spinner.

Current Behavior

Looking at the actual codebase:

  • app/models/mod.rb fetches all mods via Rails.cache.fetch("firestore/mods", expires_in: 5.minutes) — on cache miss, this hits Firestore which can take several seconds
  • app/views/mods/_mods.html.erb wraps the table in a turbo_frame_tag "mods" — Turbo handles frame updates but with no visual loading indicator
  • app/views/mods/index.html.erb has search with data-action="input->mods#search" and 200ms debounce — during the Turbo frame fetch, no loading state is shown
  • app/views/tools/index.html.erb has no Turbo frame at all — full page load with no feedback
  • The mods page has an empty state ("No mods match your query") but no loading state
  • On first visit or after cache expires, the blank period can be 2-5 seconds

Proposed Solution

1. Style the Turbo Progress Bar

Turbo has a built-in progress bar that appears at the top of the page during navigation. It just needs CSS customization to match the gold theme.

In app/assets/stylesheets/application.css:

.turbo-progress-bar {
  height: 3px;
  background-color: #f1ad1c; /* icarus-500 gold */
}

This is the easiest win — zero JS, zero markup changes. Every Turbo navigation (clicking Mods, Tools, Info, mod detail pages) gets a gold progress bar automatically.

Turbo shows the progress bar after a 500ms delay by default. To lower it for more responsive feel:

// app/javascript/application.js
Turbo.setProgressBarDelay(200)

2. Turbo Frame Loading Indicator for Search

When the search Turbo Frame is loading, Turbo adds [busy] and aria-busy="true" to the frame automatically. Use CSS to style this:

turbo-frame[busy] {
  position: relative;
}

turbo-frame[busy]::before {
  content: "";
  display: block;
  height: 2px;
  background: linear-gradient(90deg, transparent, #f1ad1c, transparent);
  animation: shimmer 1.5s ease-in-out infinite;
}

turbo-frame[busy] table {
  opacity: 0.5;
  pointer-events: none;
  transition: opacity 0.2s;
}

@keyframes shimmer {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

This dims the existing table and shows a gold shimmer bar while search results are loading — all via CSS, no JS needed.

3. Skeleton Table Rows for Initial Load

Add skeleton placeholder rows inside _mods.html.erb that show while the real data hasn't loaded yet. Use a Stimulus controller to swap them out:

<%# In _mods.html.erb, inside the <tbody> %>
<% if mods.nil? %>
  <% 8.times do %>
    <tr class="animate-pulse">
      <td class="p-2 border-t border-slate-600" colspan="2">
        <div class="h-4 w-48 rounded bg-slate-700"></div>
      </td>
      <td class="hidden p-2 border-t sm:table-cell border-slate-600">
        <div class="h-4 w-24 rounded bg-slate-700"></div>
      </td>
      <td class="hidden p-2 border-t md:table-cell border-slate-600">
        <div class="h-4 w-12 rounded bg-slate-700"></div>
      </td>
      <td class="hidden p-2 border-t xl:table-cell border-slate-600">
        <div class="h-4 w-16 rounded bg-slate-700"></div>
      </td>
      <td class="hidden p-2 border-t lg:table-cell border-slate-600">
        <div class="h-4 w-64 rounded bg-slate-700"></div>
      </td>
    </tr>
  <% end %>
<% end %>

The skeleton rows match the actual table column structure (Name, Download, Author, Version, Week, Description) with the same responsive visibility classes (hidden sm:table-cell, etc.).

4. Tools Page Skeleton Cards

The tools page uses flex-wrapped cards, not a table. Add skeleton cards matching the actual tool card layout from tools/index.html.erb:

<% if @tools.nil? || @tools.empty? %>
  <% 4.times do %>
    <div class="flex flex-col flex-auto w-1/3 p-2 m-2 border rounded-xl border-icarus-500 bg-slate-200 dark:bg-slate-800 min-h-96 animate-pulse">
      <div class="flex items-start justify-between flex-1">
        <div class="flex flex-col flex-1 gap-2">
          <div class="h-6 w-48 rounded bg-slate-700"></div>
          <div class="h-4 w-24 rounded bg-slate-700"></div>
        </div>
        <div class="h-9 w-24 rounded-md bg-slate-700"></div>
      </div>
      <div class="space-y-2 mt-4">
        <div class="h-3 w-full rounded bg-slate-700"></div>
        <div class="h-3 w-5/6 rounded bg-slate-700"></div>
        <div class="h-3 w-2/3 rounded bg-slate-700"></div>
      </div>
    </div>
  <% end %>
<% end %>

Files to Modify

File Change
app/assets/stylesheets/application.css Add Turbo progress bar color, Turbo frame busy styles, shimmer animation
app/javascript/application.js Set Turbo.setProgressBarDelay(200)
app/views/mods/_mods.html.erb Add skeleton table rows for loading state
app/views/tools/index.html.erb Add skeleton cards for loading state

Technical Notes

  • The 5-minute Firestore cache (Rails.cache.fetch) means most page loads are fast — skeletons mainly help on first visit, cache expiry, or when Firestore is slow
  • Turbo progress bar and frame [busy] styling are pure CSS — no additional JS dependencies
  • Skeleton rows use animate-pulse which is built into Tailwind — no custom animation needed
  • The skeleton layout must match the real table/card layout exactly (same column count, same responsive breakpoints) to prevent layout shift when real data loads

Design Notes

  • Skeleton placeholder color: bg-slate-700 in dark mode, bg-slate-300 in light mode — use dark: variants
  • Turbo progress bar in gold (#f1ad1c) for consistent branding
  • animate-pulse provides a subtle breathing effect — much more modern than a spinner
  • The dimmed table during search (opacity: 0.5) tells users their current results are stale while new ones load

Testing

  • Gold progress bar appears at top during Turbo navigation between pages
  • Turbo frame shows shimmer + dimmed table during search
  • Skeleton table rows appear if mods haven't loaded yet
  • Skeleton cards appear on tools page during load
  • Skeletons match real layout (no layout shift when data arrives)
  • All skeleton elements work in both dark and light mode
  • Progress bar delay is responsive (200ms, not the default 500ms)
  • No skeleton flash on fast cached loads (content appears immediately)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions