Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire
Tags: ruby, rails, javascript, webdev
Published: 2021-02-10 23:33:46

---
title: Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire
published: true
date: 2020-06-22 13:00:00 UTC
tags: ruby,rails,javascript,webdev
canonical_url: https://boringrails.com/articles/hovercards-stimulus/
---

Somewhere along the way toward our current JavaScript hellscape, programmers decided that HTML was over. We’re done with it.

The emergence of tools like [React](https://reactjs.org/docs/hello-world.html) shifted programmers away from writing HTML, instead writing [JSX](https://reactjs.org/docs/introducing-jsx.html), a fancier tag-based markup language that worked nicely inside your JavaScript.

Backends were then relegated to being dumb JSON API endpoints. Or if you were fancy and chasing upvotes, you’d use [GraphQL](https://graphql.org/)!

But HTML? Yuck!

## A Brief History of HTML-over-the-wire

One of the key pillars of Rails is to [“Value integrated systems”](https://rubyonrails.org/doctrine/#integrated-systems). While the industry moves towards microservices, highly decoupled front-ends and teams, and the siren song of Programming via LEGO Bricks, Rails leans into one system that does it all – termed the [Majestic Monolith](https://m.signalvnoise.com/the-majestic-monolith/).

Instead of rebuilding much of what already works in Rails in a client-side JavaScript MVC framework, apps like Basecamp, GitHub, and Shopify are able to achieve snappy page loads using the concept of “HTML-over-the-wire”.

In his [seminal RailsConf 2016 talk](https://www.youtube.com/watch?v=SWEts0rlezA), [Sam Stephenson](https://twitter.com/sstephenson) walks through the pieces of this stack.

By using [Turbolinks](https://github.com/turbolinks/turbolinks) (or similar libraries like [pjax](https://github.com/defunkt/jquery-pjax) or [Inertia](https://inertiajs.com/how-it-works)) and fast HTML responses (aided by caching and avoiding excessive database queries to get [sub-100ms response times](https://www.youtube.com/watch?v=eBccDerJPJE)), you could build high performance pages, while still hanging on to the understated benefits of stateless HTTP responses and server-side logic.

As Sam points out, it was truly a “Golden Age of Web Development”.

![](https://boringrails.com/images/golden-age.png)

So while much of the industry went down the JavaScript rabbit hole – creating new innovations for [reactive rendering](https://reactjs.org/docs/reconciliation.html), [functional](https://github.com/reduxjs/redux-thunk) [state management containers](https://redux.js.org/), and [approximately](https://reach.tech/router) [seventy](https://github.com/ReactTraining/react-router) [different](https://github.com/frontarm/navi) [client-side](https://blog.remix.run/p/remix-preview) [routing](https://redwoodjs.com/docs/redwood-router) [libraries](https://github.com/4Catalyzer/found) – the quiet rebellion in Rails-land was honing these techniques and plugging along building apps out of boring server-rendered HTML.

We’re seeing a renaissance of these tools in 2020 and the excitement (at least in a small corner of the Twitter!) is reaching a fever pitch as Basecamp launches HEY: a fully-featured email client with a tiny JavaScript footprint that pushed the boundaries of the HTML-over-the-wire approach.

![Tweet about HEY Javascript](https://boringrails.com/images/hey-js-tweet.png)

## Turbolinks / Stimulus 20XX: The Future

The stack in 2014-2016 was:

- Turbolinks/pjax
- Rails UJS + `js.erb` templates ([Server-generated JavaScript Responses](https://signalvnoise.com/posts/3697-server-generated-javascript-responses))
- Heavy HTML fragment caching
- Rails Asset Pipeline and [CoffeeScript](https://coffeescript.org/)

You can even trace the origin of these techniques back even further. I was recently [sent a link](https://twitter.com/chris_vannoy/status/1274682764948844545) to a nearly 15 year old REST “microformat” called [“AHAH: Asynchronous HTML and HTTP”](http://microformats.org/wiki/rest/ahah), which is an early version of the same ideas we’re so excited about today. (You shouldn’t be surprised to see [David Hansson](https://twitter.com/dhh) listed as a contributor!)

Now a “state-of-the-art” 2020 version also includes:

- [StimulusJS](https://stimulusjs.org/handbook/introduction) (see also [AlpineJS](https://github.com/alpinejs/alpine)) for lightweight event management, data binding, and “sprinkles” of behavior
- Partial updates with Turbolinks via a new `<template>` command approach (replacing `js.erb` and supporting [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP))
- Real-time Turbolinks updates via [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) (see also [StimulusReflex](https://docs.stimulusreflex.com/)/[CableReady](https://cableready.stimulusreflex.com/))
- First-party support for Webpack, ES6, and new CSS approaches like [Tailwind](https://tailwindcss.com/) and [PurgeCSS](https://purgecss.com/)

This stack is extremely powerful and the development experience allows you to really fly. You can build fast and interactive applications with a small team, all while still experiencing the joy of a 2014-era vanilla Rails codebase.

But years of a [JavaScript SPA-heavy monoculture](https://macwright.org/2020/05/10/spa-fatigue.html) have made it hard to learn about this stack. The community is filled with practitioners, using the tools to build software and businesses. There simply has not been the same level of content produced and so many of these tools are unknown and can be unapproachable.

One of the ways I can contribute is to light the way for those want to know more by showing some real-world examples (not a [TODO list](http://todomvc.com/) or a [Counter](https://wsvincent.com/react-counter/)). Once you see how you can use tools like Stimulus and HTML responses to build features where you might instead reach for a tool like React, things will start to click.

## Let’s Build Something Real: Hovercards

Hovercards show extra contextual information in a popup bubble when you hover over something in your app. You can see examples of this UI pattern on GitHub, Twitter, and even Wikipedia.

![Examples of hovercard UI](https://boringrails.com/images/hovercard-examples.png)

This feature is really easy to build with Rails using an HTML-over-the-wire approach.

Here’s the plan:

- Build a controller action to render the hovercard as HTML
- Write a tiny Stimulus controller to fetch the hovercard HTML when you hover

…and that’s it.

We don’t need to make API endpoints and figure out how to structure all of the data we need. We don’t need to reach for React or Vue to make this a client-side component.

The beauty of this boring Rails approach is that the feature is dead-simple and it’s equally straightforward to build. It’s easy to reason about the code and super extensible.

For this example, let’s build the event feed for a sneaker marketplace app.

![Shoe feed hovercard example](https://boringrails.com/images/shoe-hovercard.gif)

When you hover over a shoe, you see a picture, the name, the price, etc. Same for the user, you can see a mini-profile for each user.

### The Frontend (Stimulus + fetch)

The markup for the link looks like:

```erb
<!-- app/views/shoes/feed.html.erb -->

<div
  class="inline-block"
  data-controller="hovercard"
  data-hovercard-url-value="<%= hovercard_shoe_path(shoe) %>"
  data-action="mouseenter->hovercard#show mouseleave->hovercard#hide"
>
  <%= link_to shoe.name, shoe, class: "branded-link" %>
</div>
```

Note: we are using the APIs from the [Stimulus 2.0](https://github.com/stimulusjs/stimulus/pull/202) preview release!

One of the great features of Stimulus is that you can read the markup and understand what’s happening without diving into the JavaScript.

Without knowing anything else about the implementation, you could guess how it’s going to work: this link is wrapped in a `hovercard` controller, when you hover (via `mouseenter` and `mouseleave` events) the card is shown or hidden.

As recommended in [Writing Better Stimulus Controllers](https://boringrails.com/articles/better-stimulus-controllers/), you should pass in the URL for the hover card endpoint as a data property so that we can re-use the `hovercard_controller` for multiple types of cards. This also keeps us from having to duplicate the application routes in JavaScript.

```js
// app/javascript/controllers/hovercard_controller.js

import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["card"];
  static values = { url: String };

  show() {
    if (this.hasCardTarget) {
      this.cardTarget.classList.remove("hidden");
    } else {
      fetch(this.urlValue)
        .then((r) => r.text())
        .then((html) => {
          const fragment = document
            .createRange()
            .createContextualFragment(html);

          this.element.appendChild(fragment);
        });
    }
  }

  hide() {
    if (this.hasCardTarget) {
      this.cardTarget.classList.add("hidden");
    }
  }

  disconnect() {
    if (this.hasCardTarget) {
      this.cardTarget.remove();
    }
  }
}
```

This is all of the JavaScript we’re going to be writing for this feature: it’s only ~30 lines and we can use this for any other hovercards in the app. There isn’t really anything app specific about this controller either, you could pull it into a separate module and re-use it across projects. It’s totally generic.

The controller uses the `fetch` API to call the provided Rails endpoint, gets some HTML back, and then inserts it into the DOM. As a small improvement, we use the Stimulus `target` API for data binding to save a reference to the card so that subsequent hovers over this link can simply show/hide the markup without making another network request.

![Hovercard Network tab](https://boringrails.com/images/hovercard-network.gif)

We also choose to remove the card when leaving the page (via the `disconnect` lifecycle method), but you could also opt to hide the card instead depending on how you want caching to work.

### The Backend (Rails + Server rendered HTML)

There is nothing magic on the frontend and it’s the same story on the backend.

```rb
# config/routes.rb
Rails.application.routes.draw do
  resources :shoes do
    member do
      get :hovercard
    end
  end
end
```

Setup a route for `/shoes/:id/hovercard`

```rb
# app/controllers/shoes_controller.rb
class ShoesController < ApplicationController
  ...

  def hovercard
    @shoe = Shoe.find(params[:id])

    render layout: false
  end
end
```

Write a basic controller action, the only difference being that we set `layout: false` so that we do not use the global application layout for this endpoint.

You can even visit this path directly in your browser to quickly iterate on the content and design. The workflow gets even better when using a utility-based styling approach like Tailwind since you don’t even need to wait for your asset bundles to rebuild!

```erb
<!-- app/views/shoes/hovercard.html.erb -->

<div class="relative" data-hovercard-target="card">
  <div data-tooltip-arrow class="absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content">
    <div class="flex space-x-3 items-center w-64">
      <%= image_tag @shoe.image_url, class: "flex-shrink-0 h-24 w-24 object-cover border border-gray-200 bg-gray-100 rounded", alt: @shoe.name %>

      <div class="flex flex-col">
        <span class="text-sm leading-5 font-medium text-indigo-600">
          <%= @shoe.brand %>
        </span>

        <span class="text-lg leading-0 font-semibold text-gray-900">
          <%= @shoe.name %>
        </span>

        <span class="flex text-sm text-gray-500">
          <%= @shoe.colorway %>
          <span class="mx-1">
            &middot;
          </span>
          <%= number_to_currency(@shoe.price.to_f / 100) %>
        </span>
      </div>
    </div>
  </div>
</div>
```

The hovercard is built with a server-rended ERB template, same as any other page in the Rails app. We set the `data-hovercard-target` as a convenience to bind to this element back in the Stimulus controller.

### Finishing Touches

The `data-tooltip-arrow` allows us to add a little triangle to the bubble with a bit of CSS. You can add a library like [Popper](https://popper.js.org/) if you have more advanced needs, but this single CSS rule works great and doesn’t require any external dependencies.

```css
/* app/javascript/stylesheets/application.css */

[data-tooltip-arrow]::after {
  content: " ";
  position: absolute;
  top: 100%;
  left: 1rem;
  border-width: 2rem;
  border-color: white transparent transparent transparent;
}
```

And voila! We’ve built hovercards!

![Hovercard for Shoes](https://boringrails.com/images/shoe-closeup.gif)

If we want to add a hovercard to another model type in our application (like User profiles), it almost feels like cheating. We can use the same Stimulus controller. All we need to do is add User specific template.

```erb
<!-- app/views/users/hovercard.html.erb -->

<div class="relative" data-hovercard-target="card">
  <div data-tooltip-arrow class="absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content">
    <div class="flex space-x-3 items-center p-1">
      <%= image_tag @user.gravatar_url, class: "flex-shrink-0 h-16 w-16 object-cover bg-gray-100 rounded inset shadow-inner", alt: @user.name %>

      <div class="flex-1 flex flex-col">
        <span class="font-bold text-lg"><%= @user.name %></span>
        <div class="flex space-x-1 items-center text-sm">
          <svg class="text-orange-400 fill-current h-4 w-4" viewBox="0 0 20 20">...</svg>
          <span class="text-gray-500 italic"><%= @user.bio %></span>
        </div>
        <span class="text-gray-400 text-xs mt-1">
          Kickin' it since <%= @user.created_at.year %>
        </span>
      </div>
    </div>
  </div>
</div>
```

![Hovercard for Users](https://boringrails.com/images/user-closeup.gif)

## Taking it to the next level

If you want to expand this feature even further, there are a few ideas you might consider:

- Removing some duplication in the hovercard templates by either: extracting a Rails `partial`, using a gem like [github/view\_component](https://github.com/github/view_component), or using the Tailwind `@apply` directive to [create components](https://tailwindcss.com/docs/extracting-components/) in your stylesheets
- Animating the hovercard using [CSS transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) to fade in and out
- Adding a delay or fancy “directional aiming” (like the [Amazon mega dropdown](https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown)) so that you can move your mouse more easily to the hovercard
- [Cancel a pending AJAX request](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Aborting_a_fetch) if you move away using the `AbortController` for the `fetch` API
- Explore caching the hovercards (assuming the data is not specific to a user or session) in Rails with [Fragment Caching](https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching)

## Wrap it up

This stack is a love letter to the web. Use links and forms. Render HTML. Keep your state on the server and in the database. Let the browser handle navigation. Add sprinkles of interactivity to improve the experience. For many it feels like a step backward, but in my opinion it’s going back to the way things should be.

It’s natural to be skeptical, especially in the current climate of “JS all the things”. But you really have to give these tools a try before you really get it. Once you see that the classic ways of building software can still get the job done, it’s hard to go back to debugging `node_modules` conflicts or rebuilding HTML forms inside of this years framework du jour.

In this [year’s RailsConf remote keynote](https://railsconf.com/2020/video/david-heinemeier-hansson-keynote-interview-with-david-heinemeier-hansson), DHH talked about the [cyclical pendulum of Hegel’s dialectics](https://en.wikipedia.org/wiki/Thesis,_antithesis,_synthesis) that happens in software. New ideas are recycled and rediscovered every few years and now is a great time to hop along for the ride.

* * *
[![](https://boringrails.com/images/twitter-banner.png)](https://twitter.com/_swanson)