The most underrated Rails helper: dom_id
Tags: ruby, rails, hotwire
Published: 2022-06-29 01:18:22

---
title: The most underrated Rails helper: dom_id
published: true
date: 2022-06-28 13:00:00 UTC
tags: ruby,rails,hotwire
canonical_url: https://boringrails.com/articles/rails-dom-id-the-most-underrated-helper/
---

This post is part of [Hotwire Summer](/hotwire-summer): a new season of content on Boring Rails!

The `dom_id` helper in Rails is over a decade old, but has proven to be an invaluable concept in Hotwire.

This secret workhorse powers all kinds of HTML-related behavior in Rails. It has one key job: making it easy to associate application data with DOM elements.

`dom_id` takes two arguments: a record and an optional prefix.

The `record` can be anything that responds to `to_key` and `model_name`, but 99% of the time you are passing it an ActiveRecord model. The `prefix` can be anything that responds to `to_s`, but 99% of the time it is a symbol.

[From the docs](https://api.rubyonrails.org/classes/ActionView/RecordIdentifier.html):

```ruby
dom_id(Post.find(45)) # => "post_45"
dom_id(Post.new) # => "new_post"

dom_id(Post.find(45), :edit) # => "edit_post_45"
dom_id(Post.new, :custom) # => "custom_post"
```

The reason this helper is so nice is that it [sets a convention](https://rubyonrails.org/doctrine#convention-over-configuration). Instead of defining your own way of identifying markup and later hoping you remember the pattern, Rails provides a standard; you don’t need to switch contexts to know if you set an id to “post\_23\_comments” or “comments-post-23” or wait, was it “post\_23-comments”?

By providing stable id values for your elements, you avoid fragile code like targeting the first element child or finding a node based on the text value.

Here are places where you should regularly reach for `dom_id` when building Hotwire apps.

## Super clean tag builders

Leaning into HTML markup as the source of truth means adding extra attributes and conditionals when rendering templates. While you can do plain old string interpolation in your ERB templates, Rails has a set of [nice tag builders](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag) that help you avoid drowning in a sea of brackets, braces, and octothorpes.

```erb
<%= tag.div id: dom_id(@post, :comments), class: "flex flex-col divide-y" do %>
  <%= render @post.comments %>
<% end %>
```

As you use these helpers more, make sure you check out these tips:

- Set up the [Tailwind Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) plugin for VS Code to [autocomplete when using tag helpers](https://twitter.com/_swanson/status/1541419976732692485)
- Take advantage of [class\_name helper](https://twitter.com/_swanson/status/1341080006169137152) (from the `classNames` React API) for conditional classes
- For commmon elements, consider extracting a component with a [lightweight helper](https://boringrails.com/tips/lightweight-components-with-helpers-stimulus) or a library like [ViewComponent](https://viewcomponent.org/)

## Deep linking anchor tags

Since Rails leans so heavily on native browser features, make sure you take advantage of anchor tags on links. You can use `dom_id` to scroll the browser directly to an element with the corresponding `id` (or generate a permalink that users can share).

```erb
<%= link_to "View comment", posts_path(@post, anchor: dom_id(@comment)) %>
```

You can also use this pattern for redirects. For example, after creating an item in a list, you can redirect back to the index page but scroll to the new item.

```ruby
class CommentsController < ApplicationController
  def create
    @post.comments.create!(comment_params)

    redirect_to posts_path(@post, anchor: dom_id(@comment))
  end
end
```

A few more tips related to deep linking:

- You can use the `:target` pseudo-class to [style the element with an ID matching the URL anchor](https://developer.mozilla.org/en-US/docs/Web/CSS/:target). In Tailwind, simply use the [target prefix](https://tailwindcss.com/docs/hover-focus-and-other-states#target) (e.g. `target:bg-yellow-50` to add a subtle yellow background to an element when it matches the URL anchor)
- Another handy CSS property is `scroll-margin-top`: the browser will scroll the targeted element all the way to the top of the window. You may want a little extra padding, but only when you scrolled to the element. Don’t add extra margin or padding to your designs or add weird wrapper divs…`scroll-margin-top` ([Tailwind class](https://tailwindcss.com/docs/scroll-margin): `scroll-mt`) is the answer.

## With Turbo Frames

When you start adding Turbo Frames to your application, you’ll need to provide an id for the frame tag. The Rails `turbo_frame_tag` uses – you guessed it – `dom_id` under the hood. But you can also pass in your own ids as well.

```ruby
turbo_frame_tag @post # => <turbo-frame id="post_123"></turbo-frame>
turbo_frame_tag dom_id(@post, :comments) # => <turbo-frame id="comments_post_123"></turbo-frame>
```

Since a Turbo Frame needs to be unique per page, `dom_id` is a convenient way to generate frame ids, especially if you have multiple frames on the page.

And since you can [navigate a frame via other links](https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets), it’s a great convention to follow:

```erb
<%= turbo_frame_tag @comment, src: comment_path(@comment) %>

<!-- Elsewhere... -->
<%= link_to "Edit", edit_comment_path(@comment), data: { turbo_frame: @comment } %>
```

## Scoping Turbo Stream responses

Making small mutations on a page with Turbo Streams is super powerful, but since your stream actions are in a separate `turbo_stream.erb` file, it can be tricky to match up your ids between views.

Once again, `dom_id` keeps things consistent.

```erb
<!-- app/views/plans/quick_edit/update.turbo_stream.erb -->
<%= turbo_stream.replace dom_id(@plan, :title), partial: "plans/title" %>
<%= turbo_stream.replace dom_id(@plan, :notes), partial: "plans/notes" %>
<%= turbo_stream.replace dom_id(@plan, :assigned), partial: "plans/assigned" %>
```

And by adding the correct ids to your markup, the Turbo Stream responses are super clean:

```erb
<!-- app/views/comments/destroy.turbo_stream.erb -->
<!-- Calls `dom_id(@comment)` under the hood -->
<%= turbo_stream.remove @comment %>
```

## Wrap it up

Who would have thought that a simple helper to generate HTML `id` values from your application models would be such a useful concept that, more than a decade after first being introduced, it continues to prove helpful even on the newest and shiniest parts of Rails.

If you haven’t been using `dom_id` before, consider it the next time you write a view in your Rails app. You might be surprised at how much more pleasant it is to create HTML when you aren’t littering it with code like:

```erb
<div id='<%= "#{@post.id}-comments" %>'>
</div>
```

* * *
[![](https://boringrails.com/images/twitter-banner.png)](https://twitter.com/_swanson)