DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Update favicon with badge using custom turbo streams in Rails

This article was originally published on Rails Designer


In a previous article I showed how to update the page title with a counter using custom Turbo Stream actions. That works great when you can see the tab. But what about when the tab is just one of many? Or pinned and showing only the favicon?

This article extends that solution by adding a visual badge to the favicon itself. Same approach, same clean API, just a different target.

As always the code can be found on GitHub. It will look something like this:

Creating the favicon update action

The favicon update follows the same pattern as the title counter. The view sets the initial favicon based on the message count:

 <%% content_for :title, @count.positive? ? "#{@count} • Messages" : "Messages" %>
+<%% content_for :favicon, @count.positive? ? "./icon-unread.svg" : "./icon.svg" %>
 <%%= turbo_stream_from :messages %>
Enter fullscreen mode Exit fullscreen mode

The layout uses this to set the favicon link:

 <link rel="icon" href="/icon.png" type="image/png">
-<link rel="icon" href="/icon.svg" type="image/svg+xml">
+<link rel="icon" href="<%%= yield(:favicon) || "/icon.svg" %>" type="image/svg+xml">
 <link rel="apple-touch-icon" href="/icon.png">
Enter fullscreen mode Exit fullscreen mode

In the Turbo Stream responses, the new custom action is called alongside the title counter:

 <%%= turbo_stream.prepend "messages", partial: @message %>

 <%%= turbo_stream.set_title_counter @count %>
+
+<%%= turbo_stream.update_favicon @count %>
Enter fullscreen mode Exit fullscreen mode

Same for destroy:

 <%%= turbo_stream.remove @message %>

 <%%= turbo_stream.set_title_counter @count %>
+
+<%%= turbo_stream.update_favicon @count %>
Enter fullscreen mode Exit fullscreen mode

See how update_favicon is called just like the set_title_counter action from the previous article. This is the same clean API.

Now create the helper method for the custom Turbo Stream tag:

 module TurboStreamActionsHelper
   def set_title_counter(count, divider: nil)
     turbo_stream_action_tag :set_title_counter, count: count, divider: divider
   end
+
+  def update_favicon(count)
+    turbo_stream_action_tag :update_favicon, count: count
+  end
 end

 Turbo::Streams::TagBuilder.prepend(TurboStreamActionsHelper)
Enter fullscreen mode Exit fullscreen mode

The turbo_stream_action_tag method generates an HTML tag with the action name and attributes. This produces a tag like:

<turbo-stream action="update_favicon" count="5">
  <template></template>
</turbo-stream>
Enter fullscreen mode Exit fullscreen mode

When Turbo encounters this tag, it looks up the update_favicon function in Turbo.StreamActions and executes it. The JavaScript action is simple:

// app/javascript/turbo_stream_actions/update_favicon.js
export default function() {
  const count = this.getAttribute("count") || 0
  const faviconLink = document.querySelector("link[rel*='icon']")
  const iconPath = count > 0 ? "./icon-unread.svg" : "./icon.svg"

  faviconLink.href = iconPath
}
Enter fullscreen mode Exit fullscreen mode

This function reads the count attribute from the Turbo Stream tag. It finds the favicon link element in the document. Then it updates the href to either the unread icon or the default icon based on the count.

The logic is straightforward. If there are unread messages, show the badge icon. Otherwise show the default icon. The browser handles the rest.

Now register it with Turbo:

 import { Turbo } from "@hotwired/turbo-rails"
 import set_title_counter from "turbo_stream_actions/set_title_counter"
+import update_favicon from "turbo_stream_actions/update_favicon"

 Turbo.StreamActions.set_title_counter = set_title_counter
+Turbo.StreamActions.update_favicon = update_favicon
Enter fullscreen mode Exit fullscreen mode

The new custom action is now wired up and working. When messages are created or destroyed, the favicon updates automatically. This works for both direct user interactions and broadcasts from other users.

The favicon itself is just the default Rails favicon/icon that I adjusted with a blue badge—I know, not pretty, but it gets the job done:

<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
   <circle cx="256" cy="256" r="256" fill="red"/>

   <circle cx="382" cy="130" r="130" fill="blue"/>
</svg>
Enter fullscreen mode Exit fullscreen mode

Of course you should change it to match your favicon/icon. Typically a bright color for the badge works best!


This favicon update complements the title counter from the previous article. Adding even more actions is straightforward. You could add sound notifications, desktop notifications or any other browser API you need. The pattern stays the same. ❤️

Top comments (0)