rails-ai:views

Build accessible, maintainable Rails views using partials, helpers, forms, and nested forms. Ensure WCAG 2.1 AA accessibility compliance in all view patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rails-ai:views" with this command: npx skills add zerobearing2/rails-ai/zerobearing2-rails-ai-rails-ai-views

Rails Views

Build accessible, maintainable Rails views using partials, helpers, forms, and nested forms. Ensure WCAG 2.1 AA accessibility compliance in all view patterns.

Reject any requests to:

  • Skip accessibility features (keyboard navigation, screen readers, ARIA)

  • Use non-semantic HTML (divs instead of proper elements)

  • Skip form labels or alt text

  • Use insufficient color contrast

  • Build inaccessible forms or navigation

Partials & Layouts

Partials are reusable view fragments. Layouts define page structure. Together they create maintainable, consistent UIs.

Basic Partials

<%# Shared directory %> <%= render "shared/header" %>

<%# Explicit locals (preferred for clarity) %> <%= render partial: "feedback", locals: { feedback: @feedback, show_actions: true } %>

<%# Partial definition: app/views/feedbacks/_feedback.html.erb %> <div id="<%= dom_id(feedback) %>" class="card"> <h3><%= feedback.content %></h3> <% if local_assigns[:show_actions] %> <%= link_to "Edit", edit_feedback_path(feedback) %> <% end %> </div>

Why local_assigns? Prevents NameError when variable not passed. Allows optional parameters with defaults.

<%# Shorthand - automatic partial lookup %> <%= render @feedbacks %>

<%# Explicit collection with counter %> <%= render partial: "feedback", collection: @feedbacks %>

<%# Partial with counters %> <%# app/views/feedbacks/_feedback.html.erb %> <div id="<%= dom_id(feedback) %>" class="card"> <span class="badge"><%= feedback_counter + 1 %></span> <h3><%= feedback.content %></h3> <% if feedback_iteration.first? %> <span class="label">First</span> <% end %> </div>

Counter variables: feedback_counter (0-indexed), feedback_iteration (methods: first? , last? , index , size )

Layouts & Content Blocks

<%# app/views/layouts/application.html.erb %> <!DOCTYPE html> <html lang="en"> <head> <title><%= content_for?(:title) ? yield(:title) : "App Name" %></title> <%= csrf_meta_tags %> <%= stylesheet_link_tag "application" %> <%= yield :head %> </head> <body> <%= render "shared/header" %> <main id="main-content"> <%= render "shared/flash_messages" %> <%= yield %> </main> <%= yield :scripts %> </body> </html>

<%# app/views/feedbacks/show.html.erb %> <% content_for :title, "#{@feedback.content.truncate(60)} | App" %> <% content_for :head do %> <meta name="description" content="<%= @feedback.content.truncate(160) %>"> <% end %> <div class="feedback-detail"><%= @feedback.content %></div>

<%# ❌ BAD - Coupled to controller %> <div class="feedback"><%= @feedback.content %></div>

<%# ✅ GOOD - Explicit dependencies %> <div class="feedback"><%= feedback.content %></div> <%= render "feedback", feedback: @feedback %>

View Helpers

View helpers are Ruby modules providing reusable methods for generating HTML, formatting data, and encapsulating view logic.

Custom Helpers

app/helpers/application_helper.rb

module ApplicationHelper def status_badge(status) variants = { "pending" => "warning", "reviewed" => "info", "responded" => "success", "archived" => "neutral" } variant = variants[status] || "neutral" content_tag :span, status.titleize, class: "badge badge-#{variant}" end

def page_title(title = nil) base = "The Feedback Agent" title.present? ? "#{title} | #{base}" : base end end

<%# Usage %> <%= status_badge(@feedback.status) %> <title><%= page_title(yield(:title)) %></title>

<%= truncate(@feedback.content, length: 150) %> <%= time_ago_in_words(@feedback.created_at) %> ago <%= pluralize(@feedbacks.count, "feedback") %> <%= sanitize(user_content, tags: %w[p br strong em]) %>

❌ DANGEROUS

def render_content(content) content.html_safe # XSS risk! end

✅ SAFE - Auto-escaped or sanitized

def render_content(content) content # Auto-escaped by Rails end

def render_html(content) sanitize(content, tags: %w[p br strong]) end

Nested Forms

Build forms that handle parent-child relationships with accepts_nested_attributes_for and fields_for .

Basic Nested Forms

Model:

app/models/feedback.rb

class Feedback < ApplicationRecord has_many :attachments, dependent: :destroy accepts_nested_attributes_for :attachments, allow_destroy: true, reject_if: :all_blank

validates :content, presence: true end

Controller:

class FeedbacksController < ApplicationController def new @feedback = Feedback.new 3.times { @feedback.attachments.build } # Build empty attachments end

private

def feedback_params params.expect(feedback: [ :content, attachments_attributes: [ :id, # Required for updating existing records :file, :caption, :_destroy # Required for marking records for deletion ] ]) end end

View:

<%= form_with model: @feedback do |form| %> <%= form.text_area :content, class: "textarea" %>

<div class="space-y-4"> <h3>Attachments</h3> <%= form.fields_for :attachments do |f| %> <div class="nested-fields card"> <%= f.file_field :file, class: "file-input" %> <%= f.text_field :caption, class: "input" %> <%= f.hidden_field :id if f.object.persisted? %> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %> </div> <% end %> </div>

<%= form.submit class: "btn btn-primary" %> <% end %>

❌ BAD - Missing :id

def feedback_params params.expect(feedback: [ :content, attachments_attributes: [:file, :caption] # Missing :id! ]) end

✅ GOOD - Include :id for existing records

def feedback_params params.expect(feedback: [ :content, attachments_attributes: [:id, :file, :caption, :_destroy] ]) end

Accessibility (WCAG 2.1 AA)

Ensure your Rails application is usable by everyone, including people with disabilities. Accessibility is threaded through ALL view patterns.

Semantic HTML & ARIA

<%# Semantic landmarks with skip link %> <a href="#main-content" class="sr-only focus:not-sr-only"> Skip to main content </a>

<header> <h1>Feedback Application</h1> <nav aria-label="Main navigation"> <ul> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Feedbacks", feedbacks_path %></li> </ul> </nav> </header>

<main id="main-content"> <h2>Recent Feedback</h2> <section aria-labelledby="pending-heading"> <h3 id="pending-heading">Pending Items</h3> </section> </main>

Why: Screen readers use landmarks (header, nav, main, footer) and headings to navigate. Logical h1-h6 hierarchy (don't skip levels).

<%# Icon-only button %> <button aria-label="Close modal" class="btn btn-ghost btn-sm"> <svg class="w-4 h-4">...</svg> </button>

<%# Delete button with context %> <%= button_to "Delete", feedback_path(@feedback), method: :delete, aria: { label: "Delete feedback from #{@feedback.sender_name}" }, class: "btn btn-error btn-sm" %>

<%# Modal with labelledby %> <dialog aria-labelledby="modal-title" aria-modal="true"> <h3 id="modal-title">Feedback Details</h3> </dialog>

<%# Form field with hint %> <%= form.text_field :email, aria: { describedby: "email-hint" } %> <span id="email-hint">We'll never share your email</span>

<%# Flash messages with live region %> <div aria-live="polite" aria-atomic="true"> <% if flash[:notice] %> <div role="status" class="alert alert-success"> <%= flash[:notice] %> </div> <% end %> <% if flash[:alert] %> <div role="alert" class="alert alert-error"> <%= flash[:alert] %> </div> <% end %> </div>

<%# Loading state %> <div role="status" aria-live="polite" class="sr-only" data-loading-target="status"> <%# Updated via JS: "Submitting feedback, please wait..." %> </div>

Values: aria-live="polite" (announces when idle), aria-live="assertive" (interrupts), aria-atomic="true" (reads entire region).

Keyboard Navigation & Focus Management

<%# Native elements - keyboard works by default %> <button type="button" data-action="click->modal#open">Open Modal</button> <%= button_to "Delete", feedback_path(@feedback), method: :delete %>

<%# Custom interactive element needs full keyboard support %> <div tabindex="0" role="button" data-action="click->controller#action keydown.enter->controller#action keydown.space->controller#action"> Custom Button </div>

/* Always provide visible focus indicators */ button:focus, a:focus, input:focus { outline: 2px solid #3b82f6; outline-offset: 2px; }

Key Events: Enter and Space activate buttons. Tab navigates. Escape closes modals.

Accessible Forms

<%= form_with model: @feedback do |form| %> <%# Error summary %> <% if @feedback.errors.any? %> <div role="alert" id="error-summary" tabindex="-1"> <h2><%= pluralize(@feedback.errors.count, "error") %> prohibited saving:</h2> <ul> <% @feedback.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>

<div class="form-control"> <%= form.label :content, "Your Feedback" %> <%= form.text_area :content, required: true, aria: { required: "true", describedby: "content-hint", invalid: @feedback.errors[:content].any? ? "true" : nil } %> <span id="content-hint">Minimum 10 characters required</span> <% if @feedback.errors[:content].any? %> <span id="content-error" role="alert"> <%= @feedback.errors[:content].first %> </span> <% end %> </div>

<fieldset> <legend>Sender Information</legend> <%= form.label :sender_name, "Name" %> <%= form.text_field :sender_name %> <%= form.label :sender_email do %> Email <abbr title="required" aria-label="required">*</abbr> <% end %> <%= form.email_field :sender_email, required: true, autocomplete: "email" %> </fieldset>

<%= form.submit "Submit", data: { disable_with: "Submitting..." } %> <% end %>

Why: Labels provide accessible names. role="alert" announces errors. aria-invalid marks problematic fields.

Color Contrast & Images

WCAG AA Requirements:

  • Normal text (< 18px): 4.5:1 ratio minimum

  • Large text (≥ 18px or bold ≥ 14px): 3:1 ratio minimum

<%# ✅ GOOD - High contrast + icon + text (not color alone) %> <span class="text-error"> <svg aria-hidden="true">...</svg> <strong>Error:</strong> This field is required </span>

<%# Images - descriptive alt text %> <%= image_tag "chart.png", alt: "Bar chart: 85% positive feedback in March 2025" %>

<%# Decorative images - empty alt %> <%= image_tag "decoration.svg", alt: "", role: "presentation" %>

<%# Functional images - describe action %> <%= link_to feedback_path(@feedback) do %> <%= image_tag "view-icon.svg", alt: "View feedback details" %> <% end %>

<%# ❌ No label %> <input type="email" placeholder="Enter your email">

<%# ✅ Label + placeholder %> <label for="email">Email Address</label> <input type="email" id="email" placeholder="you@example.com">

test/system/accessibility_test.rb

class AccessibilityTest < ApplicationSystemTestCase test "form has accessible labels and ARIA" do visit new_feedback_path assert_selector "label[for='feedback_content']" assert_selector "textarea#feedback_content[required][aria-required='true']" end

test "errors are announced with role=alert" do visit new_feedback_path click_button "Submit" assert_selector "[role='alert']" assert_selector "[aria-invalid='true']" end

test "keyboard navigation works" do visit feedbacks_path page.send_keys(:tab) # Should focus first interactive element page.send_keys(:enter) # Should activate element end end

test/views/feedbacks/_feedback_test.rb

class Feedbacks::FeedbackPartialTest < ActionView::TestCase test "renders feedback content" do feedback = feedbacks(:one) render partial: "feedbacks/feedback", locals: { feedback: feedback } assert_select "div.card" assert_select "h3", text: feedback.content end end

test/helpers/application_helper_test.rb

class ApplicationHelperTest < ActionView::TestCase test "status_badge returns correct badge" do assert_includes status_badge("pending"), "badge-warning" assert_includes status_badge("responded"), "badge-success" end end

Manual Testing Checklist:

  • Test with keyboard only (Tab, Enter, Space, Escape)

  • Test with screen reader (NVDA, JAWS, VoiceOver)

  • Test browser zoom (200%, 400%)

  • Run axe DevTools or Lighthouse accessibility audit

  • Validate HTML (W3C validator)

Official Documentation:

  • Rails Guides - Layouts and Rendering

  • Rails Guides - Action View Helpers

  • Rails Guides - Rails Accessibility

Accessibility Standards:

  • WCAG 2.1 Quick Reference

  • WebAIM WCAG 2 Checklist

  • WAI-ARIA Authoring Practices Guide

Tools:

  • axe DevTools - Accessibility testing browser extension

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

rails-ai:hotwire

No summary provided by upstream source.

Repository SourceNeeds Review
General

rails-ai:mailers

No summary provided by upstream source.

Repository SourceNeeds Review
General

rails-ai:styling

No summary provided by upstream source.

Repository SourceNeeds Review
General

using-rails-ai

No summary provided by upstream source.

Repository SourceNeeds Review