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:
#idselectors should match one element; duplicates cause unpredictable styling. - HTML spec: The HTML specification requires IDs to be unique within a document.
Comments