zerobearing2

rails-ai:views

Use when building Rails view structure - partials, helpers, forms, nested forms, accessibility (WCAG 2.1 AA)

zerobearing2 38 Updated 6mo ago
GitHub

Install

npx skillscat add zerobearing2/rails-ai/rails-ai-views

Install via the SkillsCat registry.

SKILL.md

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.

- Building ANY user interface or view in Rails - Creating reusable view components and partials - Implementing forms (simple or nested) - Ensuring accessibility compliance (WCAG 2.1 AA) - Organizing view logic with helpers - Managing layouts and content blocks - **DRY Views** - Reusable partials and helpers reduce duplication - **Accessibility** - WCAG 2.1 AA compliance built-in (TEAM RULE #13: Progressive Enhancement) - **Maintainability** - Clear separation of concerns and organized code - **Testability** - Partials and helpers are easy to test - **Flexibility** - Nested forms handle complex relationships elegantly **This skill enforces:** - ✅ **Rule #8:** Accessibility (WCAG 2.1 AA compliance)

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
Before completing view work: - ✅ WCAG 2.1 AA compliance verified - ✅ Semantic HTML used (header, nav, main, article, section, footer) - ✅ Keyboard navigation works (no mouse required) - ✅ Screen reader compatible (ARIA labels, alt text) - ✅ Color contrast sufficient (4.5:1 for text) - ✅ Forms have proper labels and error messages - ✅ All interactive elements accessible - ALWAYS ensure WCAG 2.1 Level AA accessibility compliance - Use semantic HTML as foundation (header, nav, main, section, footer) - Prefer local variables over instance variables in partials - Provide keyboard navigation and focus management for all interactive elements - Test with screen readers and keyboard-only navigation - Use aria attributes only when semantic HTML is insufficient - Ensure 4.5:1 color contrast ratio for text - Thread accessibility through all patterns - Use form helpers to generate accessible forms with proper labels

Partials & Layouts

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

Basic Partials

Render partials with explicit local variables
<%# 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.

Efficiently render partials for collections
<%# 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

Customize layout sections from individual views
<%# 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>
Using instance variables in partials Creates implicit dependencies, makes partials hard to reuse and test
<%# ❌ 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

Display status badges with consistent styling
# 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>
Use built-in Rails text helpers for formatting
<%= 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]) %>
Using html_safe on user input XSS vulnerability - allows script execution
# ❌ 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

Form with has_many relationship using fields_for

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 %>
Missing :id in strong parameters for updates Rails can't identify which existing records to update, creates duplicates instead
# ❌ 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

Use semantic HTML5 elements with proper ARIA labels
<%# 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).

Provide accessible names for elements without visible text
<%# 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>
Announce dynamic content changes to screen readers
<%# 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

Ensure all interactive elements are keyboard accessible
<%# 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

Associate labels with inputs and display errors accessibly
<%= 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

Ensure sufficient color contrast and accessible 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 %>
Using placeholder as label Placeholders disappear when typing and have insufficient contrast
<%# ❌ 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">

**System Tests with Accessibility:**
# 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)

- rails-ai:hotwire - Add interactivity with Turbo and Stimulus - rails-ai:styling - Style views with Tailwind and DaisyUI - rails-ai:controllers - RESTful actions and strong parameters for form handling - rails-ai:testing - View and system testing patterns

Official Documentation:

Accessibility Standards:

Tools: