Pass all data-attributes to the shadow root elements

Please make the Polaris web components compatible with Rails Hotwire / Turbo.

We are in the process of migrating from Tailwind to Polaris Web Components and we are having a hard time since the web components are not compatible with Hotwire Turbo.

The dataset attributes are not passed to the inner <a href that exists in the ShadowRoot and Turbo doesn’t work for some reason.

The regular link works as expected.
The web component button doesn’t work.

Thank you!

1 Like

Hey @app :waving_hand: Thanks for reaching out and sharing the screenshot—it helps a lot. Thanks for flagging the migration snag here with Polaris Web and Hotwire/Turbo.

Just to clarify, are you seeing any specific errors in the browser console (e.g., Turbo-related warnings or JS issues)? Or is it more that the components render but Turbo’s dynamic updates (like Frames or Streams) just aren’t triggering? If you have a minimal code snippet or repro steps, that could help pinpoint things.

We can’t always guarantee seamless integration with third-party tools, but I’m happy to touch base internally and pass along your feedback for potential improvements.

Hope to hear from you soon - happy to take a look into this and pass along your feedback at the very least for sure.

1 Like

Hey @Alan_G,

Thank you very much for your response.
Our delete was a simple link/ahref which was using turbo_method: :delete to send a DELETE request instead of GET.

I found a workaround for the moment that is functional.
To make it work, I commented all buttons inside the popup form.

I created a new form just for the DELETE method.

I moved all submit buttons them outside the forms and I used the web-component onclick event to send the requestSubmit().

onclick="document.getElementById('edit_popup_version_form')?.requestSubmit();this.loading = true;"

I used the form="edit_popup_version_form" attribute which allows to submit a form with a button that’s outside that form, but I don’t think this works with the web-components. So the fallback method is the requestSubmit();

The regular submit button works as expected, however, the main problem was the remove button which wasn’t responding as expected since it was a regular link button that’s using the Hotwire / Turbo method of DELETE found in the data attributes.

<turbo-frame id="new_page">
  <%= form_with(model: [@popup, popup_version], url: path, id: "edit_popup_version_form", data: { turbo_frame: "_top" }) do |form| %>
    <%= render "shared/error_messages", resource: form.object %>
    <%= form.hidden_field :popup_id, class: "form-input" %>

    <div class="form-group mb-4">
      <%= form.label :title, class: "text-sm" %>
      <%= form.text_field :title, class: "form-input", placeholder: "Light Version" %>
    </div>

    <!-- All form actions are disabled and moved outside the form -->
    <div class="form-group flex justify-between" data-turbo-prefetch="false">
      <%# form.button "Save Version", class: "#{green_btn}" %>
      <%# <s-button type="submit" variant="primary" onclick="this.loading = true;">Save version</s-button> %>
      <% if form.object.persisted? %>
        <%# method: :delete, %>
        <!--<s-button href="<%= path %>" type="button" variant="primary" icon="delete" tone="critical">Delete version</s-button> -->
        <%#link_to 'Remove Version', path, data: { turbo_method: :delete, turbo_confirm: "Are you sure?", turbo_frame: "_top"  }, class: "#{red_btn}" %>
      <% end %>
    </div>
  <% end %>

  <%# DELETE FORM %>
  <%= form_with(model: [@popup, popup_version], url: path, method: :delete, id: "remove_popup_version_form", data: { turbo_frame: "_top" }) do |form| %><% end %>

  <div class="form-group flex justify-between" data-turbo-prefetch="false">
    <s-button type="submit" variant="primary" form="edit_popup_version_form" onclick="document.getElementById('edit_popup_version_form')?.requestSubmit();this.loading = true;">Save version</s-button>
    <s-button  type="button" variant="primary" form="remove_popup_version_form" onclick="document.getElementById('remove_popup_version_form')?.requestSubmit();this.loading = true;" icon="delete" tone="critical">Delete version</s-button>
  </div>
</turbo-frame>

Here is example that works as expected, but this is a simple submit.

<%= form_with url: app_charges_path(shop: @shop_origin, plan: "basic"), method: :post do |form| %>
 <s-button type="submit" variant="secondary" onclick="this.loading">Activate Plan →</s-button>
<% end %>

In conclusion, the main problem is that Hotwire/Turbo doesn’t recognize the <s-button> when it has a href with dataset-attributes such as data-turbo-method="delete" or data-turbo-confirm="Are you Sure?" or data-turbo-frame="_top" even if we pass the data attributes to the component, they are not rendered in the actual link.

Here is another working example. The href doesn’t work, so I had to use the onclick with the Turbo visit function onclick="Turbo.visit('/app/contacts', { action: 'replace'})" as it shows on Turbo docs

<s-button  
   href="/app/contacts" 
   variant="tertiary" 
   tone="neutral" 
   onclick="Turbo.visit('/app/contacts', { action: 'replace'})">
 Get free help
</s-button>

It happens that most of our projects are using StimulusReflex
and TurboBoost Comamnds and they are all using data attributes such as

I think if the data-attributes would be passed to the web-components slots it would work, but I’m not sure as I’m unable to test that.

So I’m wondering what would be the best way we could use the web-components inside our Rails apps with Hotwire and Turbo?

Thank you so much again for your time for looking into this!

After several trial & error. I can confirm the checkbox web component works as expected, even though the dataset attributes are not passed into the inner elements.

Probably, this works because input change event bubble up through the DOM tree.

These works with Turbo Boost Commands

<s-checkbox 
   label="Choose a template" 
   data-turbo-command="GuideCommand#mark_as_checked('step_1')"
   <% if current_shop.step_1 %>checked<% end %>
 ></s-checkbox>
<s-button data-turbo-command="PopupVersionCommand#remove" data-id="1" >Remove Version</s-button>

The with Hotwire/Turbo functionality doesn’t work

<s-button 
   href="<%= path %>" 
   data-turbo-method="delete" 
   turbo-confirm="Are you sure?" turbo-frame="_top">
Remove Version
</s-button>

While the regular Rails link_to works as expected

<%= link_to 'Remove Version', path, data: { turbo_method: :delete, turbo_confirm: "Are you sure?", turbo_frame: "_top"  }, class: "#{red_btn}" %>

After further testing and playing with this, I have a working example where both <s-button> components are working as expected. So I think the main issue is that Turbo doesn’t recognize the s-button as a native a#href

<turbo-frame id="new_page">
  <!-- Hidden form removing the popup version-->
  <%= form_with(model: [@popup, popup_version], url: path, method: :delete, id: "remove_popup_version_form", data: { turbo_frame: "_top" }) do |form| %><% end %>
  
  <!-- Visible form -->
  <%= form_with(model: [@popup, popup_version], url: path, id: "edit_popup_version_form", data: { turbo_frame: "_top" }) do |form| %>
    <%= render "shared/error_messages", resource: form.object %>
    <%= form.hidden_field :popup_id, class: "form-input" %>

    <div class="form-group mb-4">
      <%= form.label :title, class: "text-sm" %>
      <%= form.text_field :title, class: "form-input", placeholder: "Light Version" %>
    </div>
    
    <div class="form-group flex justify-between" data-turbo-prefetch="false">
      <s-button type="submit" variant="primary" onclick="this.loading = true;">Save version</s-button>

      <% if form.object.persisted? %>
        <s-button  type="button" variant="primary" form="remove_popup_version_form" onclick="document.getElementById('remove_popup_version_form')?.requestSubmit();this.loading = true;" icon="delete" tone="critical">Delete version</s-button>
      <% end %>
    </div>
  <% end %>

</turbo-frame>

I hope all these details help

Thank you!

Thanks @app for all of the examples and work here - I think you’re right that your last workaround might be the best option at the moment, but I’ll do some further investigation into this on my end to see if we can suggest anything else and at the very least to log the Turbo functionality integration with Polaris/App Bridge.

Speak with you as soon as I have more info on my end!

1 Like

Hey again @app, thanks again for raising this compatibility issue with Polaris web and Hotwire Turbo.

I was able to touch base with our Polaris product team and can confirm we don’t have plans to add native Turbo support at the moment.

That said, it is super helpful for us to hear about real-world use cases like this, and if you’d like, I can set up a formal feature request to track interest in better data attribute handling or general ShadowRoot improvements, just let me know!

In the meantime, I think wrapping elements in a turbo-frame for form handling (Turbo Handbook) or adjusting event listening as noted in the Turbo Drive docs (Turbo Handbook), could help bridge the gap.

Hope this helps!

Thanks @Alan_G,

I will stick with the pattern that works for now.

For visiting other app screens

<s-button  
   variant="tertiary" 
   tone="neutral" 
   onclick="Turbo.visit('/app/contacts', { action: 'replace'})">
 Get free help
</s-button>
<%= form_with(model: [@popup, popup_version], url: path, method: :delete, id: "remove_popup_version_form", data: { turbo_frame: "_top" }) do |form| %><% end %>

<s-button  type="button" variant="primary" form="remove_popup_version_form" onclick="document.getElementById('remove_popup_version_form')?.requestSubmit();this.loading = true;" icon="delete" tone="critical">Delete version</s-button>

Thank you!

I found another working solution for making <s-button> work with Turbo / Turbo Frames…

@Alan_G Is this an anti-pattern? The version looks and works as expected and it seems to be compatible with Turbo and turbo_frames.

<s-button variant="primary">
    <%= link_to "Edit page", page_path(page_id), data: { turbo_frame: "_top" } %>
</s-button>

Hey @app, glad you found something that works! Though I agree, nesting a Rails link_to inside the Polaris component isn’t ideal and could potentially cause issues down the line with accessibility, styling, or future updates for sure.

Let me touch base with our product team to see if there’s a more supported approach or if we can provide clearer guidance on Rails/Turbo integration patterns. In the meantime, your wrapper/form submission approach from earlier might be more stable long-term, even if it requires more boilerplate.

I’ll update you once I hear back!

1 Like

Thanks @Alan_G!

Will stick with the form version and the onclick=“Turbo.visit” for now.

Looking forward to hearing back from you!

I’m running into the same issues with the a links for simple page visits. Somehow, a simple <s-link href="/posts"> isn’t working with Turbo. We’re working around this through a simple StimulusJS controller.

Also, @app, you should add the turbo_confirm to the form, and it works just fine. This is how we’re using it.

    <%= form_with model: @supplier, method: :delete, data: { turbo_confirm: t(".confirm_deletion", supplier_name: @supplier.name) } do %>
      <s-stack direction="inline" justifyContent="end">
        <s-button type="submit" variant="secondary" tone="critical">
          <%= t("common.delete") %>
        </s-button>
      </s-stack>
    <% end %>

In case it’s useful, I wrote a small polyfill to overcome the problems with Turbo not detecting s-link, s-clickable, and s-button components that function as a link.

2 Likes

Hey @app and @stefan, thanks for your patience and thanks Stefan for sharing all the workarounds, super helpful.

I checked in with our Polaris product team, and they mentioned that while nesting a Rails link_to inside an s-button may technically work, it’s not something we would recommend because it essentially results in an anchor inside a button, which violates the HTML spec for buttons (no interactive content inside a button):

To stay spec‑compliant and play nicely with Turbo, I’d stick with the patterns you already validated: Turbo.visit() for navigation and a dedicated form + requestSubmit() for DELETE and other non‑GET actions.

For confirmations, set data-turbo-confirm on the form (like Stefan showed), and use data-turbo-frame where needed.

We don’t have native Turbo support on the roadmap right now, but I’m happy to file a feature request for better data‑attribution passthrough or clearer guidance. If you folks are okay with it, I’ll include your snippets and findings.

If you folks wanted to share your high-level use cases that would be super helpful as well and I can set up the feature request for you once I hear back, but in the meantime, I’d avoid the nested link pattern to prevent future issues.

Hope this helps!

1 Like

Hey @Alan_G,

I dug a little bit deeper and identified the root cause of the issues with Turbo:

  1. The addition of the target="_self" to the a element inside the web component.
  2. The prevention of the default action of the click event on the a element inside the web component.

I believe the first issue is a bug within Turbo. Natively, the browser assumes every link without a target specified to be target="_self". However, Turbo looks for any link element and ignores a link when it contains any target set.

Because Polaris is overly specific (and even sets the target attribute when the browser already expects it to be set to target="_self"), and Turbo filters out any link with a target attribute, the click isn’t registered properly.

There are two solutions to this problem:

  1. The Polaris web component doesn’t set any defaults that the browser is already expecting to be set. In other words, if there is no target specified on the web component, just don’t set the target="_self" explicitly.
  2. Fix the issue in Turbo to ensure it’s not ignoring links that it shouldn’t ignore.

For the 2nd issue, I’ll create a PR in the Turbo repository on GitHub, because I believe it’s a bug. The 1st issue, I’ll leave up to you whether or not to fix it in Polaris. Technically, Polaris isn’t incorrect; it’s just overly verbose.

After this has been resolved, there is only one other issue to resolve. It seems that the event that Polaris (retriggers?) is marked as “preventDefault = true”. This causes issues for Turbo because it checks if a link it found is “significant” and needs action from Turbo.

I do believe this is a bug in the Polaris web components. When a href attribute is provided, the click event should never be marked as “prevent default” because you want to follow the natural flow of an HTML page and navigate to another page (as your documentation also states).

The solution to this is twofold:

  • Either Polaris ensures that the event’s default is not prevented when it’s a click event on a link (for the s-button, s-clickable, and s-link elements).
  • Or ensure that the event is not marked as “default prevented” (see event.defaultPrevented for when to check if a default is prevented).

In other words, the issue boils down to (1) a bug in Turbo that needs to be addressed, and (2) a bug in Polaris that should be addressed. This way, Polaris and Turbo play nicely together.

I’ll try to address the Turbo part. Hope you can address the Polaris part.


EDIT: Opened a PR here: Don't ignore the browser's default for `a[href]` links by stefanvermaas · Pull Request #1429 · hotwired/turbo · GitHub

2 Likes

Thanks for the detailed investigation, @stefan, super helpful and much appreciated!

We don’t currently have native support for Turbo/Hotwire in Polaris Web Components, so I can’t promise changes, but that said, I’m happy to sync with our product team and share your Turbo PR to share your findings and confirm if this is indeed an issue on the Polaris side of things.

Thanks for sharing the PR there as well, if you’re open to sharing a minimal repro (even a gist just to confirm the set up and the Polaris/Turbo versions you are using), that would help us validate quickly as well.

I can’t fully confirm if it’s an issue on our end since I haven’t seen other reports of this outside of Turbo use, but I’m happy to advocate for you on my end here. Hope to speak soon!

1 Like

Thanks @stefan, @Alan_G!

The Turbo fix has been approved and merged. It will be part of the next release.

I’ll create a repository you can take a look at in regards to the event prevent default. Ideally, Polaris wouldn’t prevent a regular click on a href as shared before.

1 Like

Thanks @stefan, appreciate you working with us on this. I still can’t guarantee anything specifically on my end, but I am definitely happy to take a look at the repo and share with our product team. Hope to hear from you soon, please let me know if you’d rather set up a DM with me to share the repo and I can get that done.

1 Like