javascript erb ruby 76 lines · 3 tabs

Stimulus: autofocus the first invalid field after Turbo update

Shared by codesnips Jan 2026
3 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["field"]

  connect() {
    this.focusFirstInvalid = this.focusFirstInvalid.bind(this)
    document.addEventListener("turbo:frame-render", this.focusFirstInvalid)
    // Handle the initial render too (server may return errors on first paint).
    this.focusFirstInvalid()
  }

  disconnect() {
    document.removeEventListener("turbo:frame-render", this.focusFirstInvalid)
  }

  focusFirstInvalid() {
    const invalid = this.element.querySelector(
      '[aria-invalid="true"], .field-error input, .field-error textarea, .field-error select'
    )
    if (!invalid) return

    requestAnimationFrame(() => {
      invalid.focus({ preventScroll: true })
      invalid.scrollIntoView({ behavior: "smooth", block: "center" })
    })
  }
}
3 files · javascript, erb, ruby Explain with highlit

This snippet shows how a small Stimulus controller improves form-validation feedback in a Hotwire/Turbo application. When a server-rendered form comes back with validation errors, Turbo replaces the form's frame in place; there is no full page load, so the browser never re-applies the usual autofocus behavior and the user is left staring at an error banner with no obvious place to fix things. The controller in focus_invalid_controller.js closes that gap by scanning the freshly rendered markup for the first field marked invalid and moving focus (and the scroll viewport) to it.

The controller registers a static targets list of field elements and listens for the Turbo lifecycle rather than DOM ready. In connect, it binds focusFirstInvalid to turbo:frame-render, which fires every time a turbo-frame swaps in new content, so the logic re-runs on each failed submission without any manual wiring. disconnect removes the listener to avoid leaks when the controller element is torn down.

The core focusFirstInvalid method prefers ARIA state over CSS classes: it queries for [aria-invalid="true"], falling back to .field-error input, so the behavior is driven by the same attributes that assistive technology reads. Once found, it calls focus() with preventScroll and then scrollIntoView with smooth centering, which gives a controlled, non-jarring movement instead of the browser's abrupt default jump. A requestAnimationFrame wrapper ensures the element is actually laid out before focus is attempted, avoiding a race where the frame content is present in the DOM but not yet focusable.

The accompanying _form.html.erb shows the Rails side: data-controller, the turbo_frame_tag, and how each field stamps aria-invalid from object.errors. The field-error wrapper class keeps the CSS fallback selector meaningful. Because the attribute is rendered by the server, the client stays a pure presentation concern and never needs to know the validation rules. The trade-off is a dependence on consistent ARIA markup; if a template forgets aria-invalid, the field simply will not be focused, which fails safe rather than throwing. This pattern is worth reaching for whenever inline validation lives inside a Turbo Frame and keyboard and screen-reader users need to land on the problem immediately.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Stimulus: autofocus the first invalid field after Turbo update — share card
Link copied