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 %>
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">
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 %>
Same for destroy:
<%%= turbo_stream.remove @message %>
<%%= turbo_stream.set_title_counter @count %>
+
+<%%= turbo_stream.update_favicon @count %>
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)
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>
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
}
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
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>
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)