Skip to main content
โšก Calmops

Avoiding Duplicate HTML IDs in Rails Forms

Introduction

HTML IDs must be unique within a page. When you render multiple Rails forms for the same model on one page (e.g., a product listing where each product has its own “Add to Cart” form), Rails generates the same IDs for each form’s fields โ€” breaking accessibility, JavaScript selectors, and label associations.

The Problem

When you render the same form partial multiple times, Rails generates identical IDs:

<% @products.each do |product| %>
  <%= form_for(@cart) do |f| %>
    <%= f.hidden_field :product_id, value: product.id %>
    <%# Generates: <input id="cart_product_id" ...> โ€” SAME for every product! %>
    <%= f.number_field :amount %>
    <%# Generates: <input id="cart_amount" ...> โ€” SAME for every product! %>
  <% end %>
<% end %>

This produces invalid HTML and causes issues with:

  • CSS selectors targeting #cart_amount
  • JavaScript document.getElementById('cart_amount')
  • <label for="..."> associations (accessibility)

The Fix: namespace Option

Add a namespace: option to form_for (or form_with). Rails prepends the namespace to all generated IDs:

<h1>Products</h1>

<div class="products">
  <% @products.each do |product| %>
    <div class="product">
      <h2><%= product.name %></h2>
      <p><%= product.description %></p>
      <p>$<%= product.price %></p>

      <%= form_for(@cart, namespace: product.id) do |f| %>
        <%= f.hidden_field :product_id, value: product.id %>
        <%# Generates: <input id="product_42_cart_product_id" ...> โ€” unique! %>

        <%= f.number_field :amount, min: 1, max: 100, value: 1 %>
        <%# Generates: <input id="product_42_cart_amount" ...> โ€” unique! %>

        <%= f.submit "Add to Cart", class: 'btn btn-primary' %>
      <% end %>
    </div>
  <% end %>
</div>

With namespace: product.id, each form’s fields get IDs like 42_cart_product_id, 43_cart_product_id, etc. โ€” all unique.

Modern Rails: form_with

In Rails 5.1+, form_with is the preferred helper. It also supports namespace::

<% @products.each do |product| %>
  <%= form_with(model: @cart, namespace: "product_#{product.id}") do |f| %>
    <%= f.hidden_field :product_id, value: product.id %>
    <%= f.number_field :amount, min: 1, max: 100, value: 1 %>
    <%= f.submit "Add to Cart" %>
  <% end %>
<% end %>

Using a Partial with a Namespace

Extract the form to a partial and pass the namespace:

<%# app/views/products/index.html.erb %>
<% @products.each do |product| %>
  <%= render 'cart_form', product: product, namespace: "product_#{product.id}" %>
<% end %>
<%# app/views/products/_cart_form.html.erb %>
<%= form_with(model: @cart, namespace: namespace) do |f| %>
  <%= f.hidden_field :product_id, value: product.id %>
  <%= f.number_field :amount, min: 1, max: 100, value: 1 %>
  <%= f.submit "Add to Cart" %>
<% end %>

Alternative: Use data-* Attributes Instead of IDs

For JavaScript targeting, prefer data-* attributes over IDs โ€” they don’t need to be unique:

<% @products.each do |product| %>
  <div class="product" data-product-id="<%= product.id %>">
    <%= form_with(model: @cart, namespace: "product_#{product.id}") do |f| %>
      <%= f.hidden_field :product_id, value: product.id %>
      <%= f.number_field :amount,
            min: 1, max: 100, value: 1,
            data: { product: product.id } %>
      <%= f.submit "Add to Cart" %>
    <% end %>
  </div>
<% end %>
// Target by data attribute โ€” no ID needed
document.querySelectorAll('[data-product]').forEach(input => {
  input.addEventListener('change', (e) => {
    console.log('Product:', e.target.dataset.product, 'Amount:', e.target.value);
  });
});

Checking for Duplicate IDs

You can audit your rendered HTML for duplicate IDs with JavaScript:

// Run in browser console to find duplicate IDs
const ids = Array.from(document.querySelectorAll('[id]')).map(el => el.id);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
console.log('Duplicate IDs:', [...new Set(duplicates)]);

Or use an HTML validator like the W3C Markup Validator.

Why Unique IDs Matter

  • Accessibility: <label for="id"> must match exactly one element. Screen readers rely on this.
  • JavaScript: document.getElementById() returns only the first match โ€” silently ignoring duplicates.
  • CSS: #id selectors should match one element; duplicates cause unpredictable styling.
  • HTML spec: The HTML specification requires IDs to be unique within a document.

Resources

Comments