User-Specific Content in Turbo Stream Partials

How would you conditionally show or hide user-specific content in a partial sent over Turbo Stream? Think scenarios like showing edit actions only for messages authored by the current user or displaying admin controls based on user permissions.

This is particularly tricky when dealing with Turbo Streams, where the same partial might be rendered for different users with different permissions. Let’s look at a common scenario: displaying edit and delete actions only for messages authored by the current user.

This is a case I recently had to tackle with Rails Designers (soon available to use/host yourself too! 🤫).

Initially, you might handle this with standard Rails conditionals in your view:

<ul>
  <% @messages.each do |message| %>
    <li>
      <small>From <%= message.author.name %> (<%= message.author.id %>)</small>

      <p>
        <%= message.content %>
      </p>

      <% if Current.user.id == message.author.id %>
        <ul>
          <li><%= link_to "Edit Message", "#" %></li>
          <li><%= link_to "Delete Message", "#" %></li>
        </ul>
      <% end %>

      <hr>
    </li>
  <% end %>
</ul>

This works fine for regular page loads, but fails with Turbo Streams. When you broadcast updates to multiple users, the server-side conditional logic runs with whatever user context was active during the broadcast—not necessarily the receiving user’s context.

Introducing the turbo-show Element

(Full source available on GitHub)

(how the app in in above GitHub works—only the “current user” can edit/delete their own messages)

Instead of handling this logic server-side, you can push the conditional rendering to the client using a custom HTML element. Here’s how I solve this with a turbo-show element:

<ul>
  <% @messages.each do |message| %>
    <li>
      <small>From <%= message.author.name %> (<%= message.author.id %>)</small>

      <p>
        <%= message.content %>
      </p>

      <turbo-show when="current-user-id" is="<%= message.author.id %>">
        <ul>
          <li><%= link_to "Edit Message", "#" %></li>
          <li><%= link_to "Delete Message", "#" %></li>
        </ul>
      </turbo-show>

      <hr>
    </li>
  <% end %>
</ul>

The turbo-show element checks if the current user’s id (stored in a meta tag) matches the message author’s id. If not, the entire element and its contents are removed from the DOM.

See how by just reading the HTML you understand what is happening? “Turbo show when current user id is message author id”. 😘👌

The Custom Element Implementation

Turbo itself uses custom elements extensively (like turbo-frame and turbo-stream), so I am following the same pattern. Here’s the complete turbo-show implementation:

class TurboShow extends HTMLElement {
  static #operators = {
    is: (content, value) => content === value
    // extend the "operators" as needed, e.g. "contains", "is_not", etc.
  }

  connectedCallback() {
    if (this.#removable()) this.remove()
  }

  // private

  #removable() {
    if (this.#whenAttributeMissing()) return true
    if (this.#metaTagMissing()) return true
    if (this.#unmetConditions()) return true

    return false
  }

  #whenAttributeMissing() {
    return !this.hasAttribute("when")
  }

  #metaTagMissing() {
    return !this.#metaTag
  }

  #unmetConditions() {
    const operators = Object.keys(TurboShow.#operators)
      .filter(operator => this.hasAttribute(operator))

    if (operators.length === 0) return false

    return operators.some(operator => {
      const value = this.getAttribute(operator)
      const check = TurboShow.#operators[operator]

      return !check(this.#metaContent, value)
    })
  }

  get #metaTag() {
    return document.querySelector(`meta[name="${this.getAttribute("when")}"]`)
  }

  get #metaContent() {
    return this.#metaTag.getAttribute("content")
  }
}

customElements.define("turbo-show", TurboShow)

I think it is quite readable, but the element works by:

  1. looking for a meta tag with the name specified in the when attribute;
  2. comparing the meta tag’s content with the value in operator attributes (like is);
  3. removing itself from the DOM if conditions aren’t met.

You’ll also need to add the information, like the current user’s id, to your layout’s meta tags:

<meta name="current-user-id" content="<%= current_user.id %>" />

To make this work in your Rails application, first add the library path to your importmap in config/importmap.rb:

pin_all_from "app/javascript/library", under: "library"

Then import the custom element in your app/javascript/application.js:

import "library/turbo_show"

This approach gives you clean, client-side conditional rendering that works smoothly with Turbo Streams. The same partial can be broadcast to multiple users, with each client only showing the appropriate content based on their individual context. Of course the actual endpoints still need to have the proper authorization checks!

The #operators hash can easily be extended to support more complex conditions like contains, is_not, or greater_than, making this a flexible solution for various conditional rendering scenarios.

This technique gives you many possibilities for user-specific content in your Rails app:

  • Role-based permissions: show admin controls only to administrators by checking against a user-role meta tag with is="admin";
  • Feature flags: toggle new features for specific users or plans using when="feature-flags" and contains="beta-feature";
  • Subscription tiers: display premium content or upgrade prompts based on the user’s subscription level with when="subscription-tier" and is="pro";
  • Team membership: show team-specific actions only to team members by comparing when="team-id" with the current context;
  • Geographic restrictions: hide or show content based on user location using when="user-country" and appropriate operators;
  • A/B testing: conditionally render different UI variants based on experiment groups stored in meta tags.

I have thought of making this an open-source tool too (like I did with Turbo Transition; that was also extracted from Rails Designers), but I already have a growing list of OSS projects (to come). Feel free to take the ideas and create your own NPM package though. Let me know about it and I gladly would link to it.

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

More articles like this on modern Rails & frontend? Get them first in your inbox.
JavaScript for Rails Developers
Out now

UI components Library for Ruby on Rails apps

$ 99 one-time
payment

View components