Overview
Progressive enhancement represents a foundational strategy in web development where applications start with a universally accessible baseline experience and incrementally add advanced features for environments that support them. This approach inverts the traditional development model by prioritizing core functionality over visual polish or advanced interactions.
The strategy emerged from the recognition that web applications face heterogeneous runtime environments: varying browser capabilities, network conditions, device specifications, and user preferences. Rather than requiring all users to meet maximum technical requirements, progressive enhancement ensures the application remains functional across the spectrum of environments while delivering superior experiences where possible.
At its core, progressive enhancement distinguishes between essential content and functionality versus enhancements. Essential elements include semantic HTML content, critical navigation, form submission capabilities, and core business logic. Enhancements encompass CSS styling, JavaScript interactions, animations, advanced form controls, and optimization features like lazy loading or prefetching.
# Basic form submission works without JavaScript
# Enhanced version adds client-side validation and AJAX
class ArticlesController < ApplicationController
def create
@article = Article.new(article_params)
respond_to do |format|
if @article.save
format.html { redirect_to @article }
format.turbo_stream # Enhanced experience for Turbo-capable browsers
format.json { render json: @article, status: :created }
else
format.html { render :new, status: :unprocessable_entity }
format.turbo_stream { render :form_error }
format.json { render json: @article.errors, status: :unprocessable_entity }
end
end
end
end
This controller responds to multiple formats, providing a traditional HTML redirect as baseline behavior while offering enhanced Turbo Stream responses for browsers supporting that technology. The application functions correctly regardless of JavaScript availability.
Progressive enhancement contrasts with graceful degradation, where developers build for modern environments then attempt to patch functionality for older ones. Progressive enhancement reverses this priority: baseline functionality comes first, enhancements layer on top. This shift in development sequence affects architectural decisions, testing strategies, and feature implementation.
Key Principles
Progressive enhancement operates on several interconnected principles that guide implementation decisions and architectural patterns.
Content Layer Priority establishes semantic HTML as the foundation. Every page must deliver meaningful content through properly structured markup independent of styling or scripting. This principle ensures screen readers, search engines, and minimal browsers access core information. HTML provides structure and meaning; other layers add presentation and behavior.
<!-- Semantic structure works without CSS or JavaScript -->
<article>
<header>
<h1>Article Title</h1>
<time datetime="2025-01-15">January 15, 2025</time>
</header>
<section>
<p>Article content remains accessible...</p>
</section>
<footer>
<a href="/articles">Back to Articles</a>
</footer>
</article>
Functional Baseline requires core application operations to work without JavaScript. Forms submit through standard HTTP requests. Navigation functions through hyperlinks. Data persists through server-side processing. JavaScript enhancement may improve these interactions but never gates basic functionality.
Feature Detection Over Browser Sniffing determines capability support at runtime rather than inferring from user agent strings. The application tests for specific feature availability and conditionally applies enhancements. This approach handles browser diversity more reliably than maintaining user agent detection matrices.
// Feature detection example
if ('IntersectionObserver' in window) {
// Apply lazy loading enhancement
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
}
// Images load normally without IntersectionObserver
Layer Independence maintains separation between content (HTML), presentation (CSS), and behavior (JavaScript). Each layer enhances without requiring others. CSS fails gracefully when unsupported properties encounter older browsers. JavaScript enhancements degrade to baseline HTML functionality when unavailable.
Server-Side Foundation establishes the server as the source of truth for application state and business logic. Client-side code optimizes user experience but never becomes mandatory for application operation. This principle prevents situations where JavaScript failures render applications unusable.
Accessibility First recognizes that progressive enhancement naturally supports accessibility requirements. Semantic HTML provides screen reader navigation. Keyboard interaction works through standard HTML controls. ARIA attributes enhance but don't replace proper semantic markup.
Network Resilience acknowledges unreliable network conditions affect enhancement delivery. Critical content loads through initial HTML response. Enhancements tolerate failed requests for secondary resources. The application degrades functionality appropriately when enhancement resources fail to load.
These principles interconnect: content layer priority enables functional baseline, which combines with server-side foundation to ensure network resilience. Feature detection allows safe enhancement application while maintaining layer independence.
Implementation Approaches
Progressive enhancement manifests through several implementation strategies, each addressing different aspects of the baseline-to-enhanced spectrum.
Server-Rendered Foundation with Client Hydration generates complete HTML on the server, delivering immediately functional pages. JavaScript then "hydrates" this markup, attaching event handlers and enabling interactive features. This approach provides instant baseline functionality while preparing for enhanced interactions.
The server renders full page content including forms, navigation, and data displays. Users interact with this content immediately without waiting for JavaScript parsing and execution. Once JavaScript loads, the framework attaches listeners to existing DOM elements rather than regenerating content client-side.
# Server generates complete HTML
class DashboardController < ApplicationController
def index
@metrics = Metric.recent.includes(:category)
@alerts = Alert.active.order(created_at: :desc)
# View renders complete HTML table
# JavaScript optionally enhances with sorting, filtering
end
end
Form Submission Progression starts with standard HTML form submission, then layers enhancements. The baseline submits forms via POST requests, redirecting on success. Enhanced versions add client-side validation, inline error display, and AJAX submission with partial page updates.
This strategy ensures forms function in any environment while providing superior experiences where possible. Server-side validation remains mandatory regardless of client enhancements since client validation only improves user experience.
# View supports both standard and enhanced submission
<%= form_with model: @article, data: { turbo: true } do |f| %>
<%= f.text_field :title, required: true %>
<%= f.text_area :content, required: true %>
<%= f.submit "Save Article" %>
<% end %>
# Standard submission: full page reload
# Enhanced: Turbo intercepts, submits via fetch, updates page section
CSS Feature Layering applies baseline styles universally, then adds enhancements through feature queries. The application styles content with broadly supported properties first. Feature queries test for advanced property support, applying enhanced styles only where available.
/* Baseline layout works everywhere */
.grid {
display: block;
}
.grid-item {
margin-bottom: 1rem;
}
/* Enhanced layout for supporting browsers */
@supports (display: grid) {
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.grid-item {
margin-bottom: 0;
}
}
JavaScript Module Loading separates baseline scripts from enhancements. Core functionality loads synchronously or in document head. Enhancement modules load asynchronously with appropriate fallback handling. This prevents enhancement script failures from blocking baseline operation.
The application marks enhancement scripts with async or defer attributes, preventing render blocking. Module imports use dynamic imports for code-splitting, loading enhancement features only when needed.
API Response Format Negotiation returns different response formats based on request characteristics. HTML requests receive complete pages. Enhanced clients request JSON or other formats for partial updates. The server handles both through format-specific response logic.
def show
@project = Project.find(params[:id])
respond_to do |format|
format.html # Full page render
format.json { render json: @project } # API response
format.turbo_stream # Partial update
end
end
Component Enhancement Strategy builds components with HTML-only versions, then attaches JavaScript behaviors progressively. A dropdown menu functions as a details/summary element or CSS-only implementation in baseline form. JavaScript enhancement adds keyboard navigation, ARIA management, and transition effects.
This approach requires components to exist meaningfully in HTML before enhancement. Custom elements may define baseline behavior through HTML content while registering enhanced capabilities when script loads.
Conditional Asset Loading delivers different asset bundles based on detected capabilities or explicit user preferences. Modern browsers receive optimized bundles with advanced features. Older environments receive compatibility bundles with broader support. This reduces payload for capable browsers while maintaining functionality elsewhere.
Service workers detect browser capabilities during installation, caching appropriate asset versions. The application serves different manifest files or uses dynamic imports based on feature detection results.
Ruby Implementation
Ruby web frameworks, particularly Rails, provide specific mechanisms supporting progressive enhancement patterns. These tools enable developers to build applications that work across capability spectrums while writing maintainable code.
Unobtrusive JavaScript Helpers generate HTML that functions without JavaScript while supporting enhancement when available. Rails form helpers create standard HTML forms with appropriate attributes for JavaScript frameworks to enhance.
# generates functional form that submits normally
# Turbo/Stimulus enhance without modifying Ruby code
<%= form_with model: @comment, local: false do |f| %>
<%= f.text_area :body, rows: 3 %>
<%= f.submit "Post Comment" %>
<% end %>
# Generates:
# <form data-remote="true" action="/comments" method="post">
# <textarea name="comment[body]" rows="3"></textarea>
# <input type="submit" value="Post Comment">
# </form>
The data-remote attribute signals JavaScript enhancement opportunity without requiring it. Standard form submission works when JavaScript is unavailable. Turbo intercepts these submissions when present, providing enhanced experience.
Turbo Frames and Streams update page sections without full reloads while falling back to standard navigation. Turbo Frames wrap page sections, loading content updates within frame boundaries. When JavaScript is absent, frame navigation triggers full page loads.
# Controller responds to both HTML and Turbo Stream requests
class NotificationsController < ApplicationController
def create
@notification = current_user.notifications.create(notification_params)
respond_to do |format|
format.html { redirect_to notifications_path }
format.turbo_stream {
render turbo_stream: turbo_stream.prepend(
"notifications",
partial: "notification",
locals: { notification: @notification }
)
}
end
end
end
<!-- View includes turbo frame -->
<turbo-frame id="notifications">
<%= render @notifications %>
</turbo-frame>
<!-- Enhanced: updates frame without page reload -->
<!-- Baseline: links inside frame navigate normally -->
Respond_to Format Negotiation handles multiple request formats, providing appropriate responses for different client capabilities. The server inspects Accept headers and request formats, routing to format-specific rendering logic.
class SearchController < ApplicationController
def index
@results = Search.query(params[:q])
respond_to do |format|
# Traditional browser request
format.html
# AJAX request from enhanced client
format.json { render json: @results }
# Turbo-enhanced request
format.turbo_stream {
render turbo_stream: turbo_stream.replace(
"results",
partial: "results",
locals: { results: @results }
)
}
end
end
end
This pattern ensures the server provides functional responses regardless of client enhancement level. Each format receives appropriate content: complete HTML for baseline, JSON for API clients, Turbo Streams for enhanced interfaces.
Stimulus Controllers for Progressive Behavior attach JavaScript behaviors to existing HTML markup rather than generating UI through JavaScript. Stimulus finds elements with data attributes, connecting controller logic while leaving HTML functional.
# View generates standard HTML
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle">
Menu
</button>
<ul data-dropdown-target="menu" hidden>
<li><a href="/profile">Profile</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</div>
# Stimulus enhances with toggle behavior
# Without JavaScript: CSS or details/summary element provides similar function
ViewComponent with Fallback Rendering creates reusable components that specify baseline and enhanced rendering modes. Components detect request context and render appropriately.
class NavigationComponent < ViewComponent::Base
def initialize(items:, enhanced: false)
@items = items
@enhanced = enhanced
end
def render
if @enhanced && turbo_request?
render_turbo_navigation
else
render_standard_navigation
end
end
private
def render_standard_navigation
# Standard HTML list with links
tag.nav do
tag.ul do
safe_join(@items.map { |item|
tag.li { link_to item[:text], item[:path] }
})
end
end
end
def render_turbo_navigation
# Enhanced with Turbo Frame loading
tag.nav data: { controller: "navigation" } do
# Enhanced markup
end
end
end
Helper Methods for Capability Detection check request characteristics and user preferences, conditionally applying enhancements.
module ApplicationHelper
def enhanced_user?
# Check for enhancement support
turbo_native_app? ||
cookies[:enhanced_mode] == "true" ||
request.user_agent.match?(/Modern-Browser-Pattern/)
end
def render_with_fallback(enhanced_partial, baseline_partial, locals = {})
partial = enhanced_user? ? enhanced_partial : baseline_partial
render partial: partial, locals: locals
end
end
ActionCable for Progressive Realtime Features adds realtime updates without breaking baseline request/response patterns. Pages function through standard HTTP requests. ActionCable enhances by pushing updates through WebSocket connections when available.
class CommentsChannel < ApplicationCable::Channel
def subscribed
# Enhanced clients subscribe to updates
stream_from "post_#{params[:post_id]}_comments"
end
end
class CommentsController < ApplicationController
def create
@comment = @post.comments.create(comment_params)
# Baseline response
respond_to do |format|
format.html { redirect_to @post }
# Enhanced response includes broadcast
format.turbo_stream do
broadcast_comment
render turbo_stream: turbo_stream.append("comments", @comment)
end
end
end
private
def broadcast_comment
# Enhanced clients receive realtime update
ActionCable.server.broadcast(
"post_#{@post.id}_comments",
{
action: "append",
target: "comments",
html: render_to_string(partial: "comment", locals: { comment: @comment })
}
)
end
end
Practical Examples
Progressive enhancement manifests in concrete scenarios across different application features. These examples demonstrate baseline-to-enhanced progression in production contexts.
Interactive Data Table starts with standard HTML table rendering server-side. The baseline displays data in semantic table markup with standard sorting through URL parameters. Enhanced version adds client-side sorting, filtering, and pagination without server requests.
# Controller renders sortable data
class ProductsController < ApplicationController
def index
@products = Product.order(sort_column => sort_direction)
.page(params[:page])
respond_to do |format|
format.html # Standard pagination
format.json { render json: @products } # Enhanced filtering
end
end
private
def sort_column
Product.column_names.include?(params[:sort]) ? params[:sort] : "name"
end
def sort_direction
%w[asc desc].include?(params[:direction]) ? params[:direction] : "asc"
end
end
<!-- Baseline table with sort links -->
<table data-controller="sortable-table">
<thead>
<tr>
<th>
<%= link_to "Name", products_path(sort: "name", direction: "asc") %>
</th>
<th>
<%= link_to "Price", products_path(sort: "price", direction: "asc") %>
</th>
</tr>
</thead>
<tbody>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= number_to_currency(product.price) %></td>
</tr>
<% end %>
</tbody>
</table>
<%= paginate @products %>
<!-- JavaScript enhances with client-side sorting -->
<!-- Baseline: clicking headers navigates with query params -->
Image Gallery with Lazy Loading displays images through standard img elements initially. Enhanced version defers loading off-screen images until scrolling approaches, reducing initial page weight.
class GalleryController < ApplicationController
def show
@gallery = Gallery.includes(images: :attachments).find(params[:id])
@images = @gallery.images.order(:position)
end
end
<!-- Baseline: all images load immediately -->
<div class="gallery" data-controller="lazy-loader">
<% @images.each do |image| %>
<figure>
<% if lazy_loading_supported? %>
<img data-lazy-loader-target="image"
data-src="<%= url_for(image.file) %>"
alt="<%= image.alt_text %>"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E">
<% else %>
<img src="<%= url_for(image.file) %>"
alt="<%= image.alt_text %>">
<% end %>
<figcaption><%= image.caption %></figcaption>
</figure>
<% end %>
</div>
<!-- Enhanced: IntersectionObserver loads images on scroll -->
<!-- Baseline: standard img elements load immediately -->
Form Validation Progression demonstrates multi-level validation implementation. Server-side validation always runs, preventing invalid data. HTML5 attributes provide basic client-side validation. JavaScript adds sophisticated inline validation with real-time feedback.
class UserRegistrationController < ApplicationController
def create
@user = User.new(user_params)
# Server validation is mandatory
if @user.save
respond_to do |format|
format.html { redirect_to dashboard_path }
format.turbo_stream {
render turbo_stream: turbo_stream.replace(
"registration_form",
partial: "success"
)
}
end
else
respond_to do |format|
format.html { render :new, status: :unprocessable_entity }
format.turbo_stream {
render turbo_stream: turbo_stream.replace(
"registration_form",
partial: "form",
locals: { user: @user }
)
}
end
end
end
end
<%= form_with model: @user,
data: { controller: "validation" } do |f| %>
<%= f.text_field :email,
required: true,
type: "email",
data: {
validation_target: "field",
action: "blur->validation#validateEmail"
} %>
<span data-validation-target="emailError"></span>
<%= f.password_field :password,
required: true,
minlength: 8,
data: {
validation_target: "field",
action: "input->validation#validatePassword"
} %>
<span data-validation-target="passwordError"></span>
<%= f.submit "Register" %>
<% end %>
<!-- Three validation levels:
1. HTML5 attributes (required, type, minlength)
2. Stimulus inline validation
3. Server-side validation (always runs) -->
Search with Autocomplete Enhancement implements search starting with traditional form submission. Enhanced version adds autocomplete suggestions as users type, loading suggestions through AJAX. Both versions execute actual search server-side.
class SearchController < ApplicationController
def index
@query = params[:q]
@results = SearchService.call(@query) if @query.present?
respond_to do |format|
format.html # Full search results page
format.json {
# Autocomplete suggestions
suggestions = SearchService.suggestions(@query)
render json: suggestions
}
end
end
end
<%= form_with url: search_path,
method: :get,
data: { controller: "autocomplete" } do |f| %>
<%= f.text_field :q,
placeholder: "Search...",
autocomplete: "off",
data: {
autocomplete_target: "input",
action: "input->autocomplete#suggest"
} %>
<div data-autocomplete-target="suggestions" hidden>
<!-- Enhanced: suggestions appear here -->
</div>
<%= f.submit "Search" %>
<% end %>
<!-- Baseline: form submits to search#index -->
<!-- Enhanced: shows suggestions, still submits for full search -->
Common Patterns
Progressive enhancement establishes recurring patterns across different implementation contexts. These patterns provide tested approaches for common enhancement scenarios.
The Fallback Link Pattern uses links as baseline navigation with JavaScript enhancement. Links point to functional endpoints that return complete pages. JavaScript intercepts clicks, loads content via AJAX, updates page sections.
# Link points to real endpoint
<%= link_to "View Details",
product_path(@product),
data: {
controller: "ajax-content",
action: "click->ajax-content#load",
ajax_content_target_value: "#product-details"
} %>
# JavaScript intercepts but link works without JS
# Endpoint returns both full page and partial content
def show
@product = Product.find(params[:id])
respond_to do |format|
format.html # Full page
format.turbo_stream { render partial: "details" }
end
end
The Details/Summary Pattern provides disclosure widgets through native HTML elements. The details element creates expandable sections without JavaScript. Enhanced versions add animations, state persistence, or coordinated accordion behavior.
<details data-controller="enhanced-disclosure">
<summary>Additional Information</summary>
<div data-enhanced-disclosure-target="content">
<%= render "additional_info" %>
</div>
</details>
<!-- Baseline: native details/summary functionality -->
<!-- Enhanced: animations, analytics tracking, state saving -->
The Form Hijacking Pattern intercepts form submissions while maintaining standard submission capability. Forms POST to proper endpoints with full server-side processing. JavaScript prevents default submission, sends AJAX request, handles response.
<%= form_with model: @comment,
data: {
controller: "remote-form",
action: "submit->remote-form#handleSubmit"
} do |f| %>
<%= f.text_area :body %>
<%= f.submit "Post" %>
<% end %>
# Stimulus controller
class RemoteFormController extends Controller {
async handleSubmit(event) {
event.preventDefault()
const form = event.target
try {
const response = await fetch(form.action, {
method: form.method,
body: new FormData(form)
})
if (response.ok) {
// Enhanced: update page section
this.updateContent(await response.text())
}
} catch (error) {
// Error: fall back to standard submission
form.submit()
}
}
}
The Progressive Loading Pattern renders critical content in initial HTML, defers secondary content loading. The page displays essential information immediately. JavaScript triggers additional content requests as needed.
class DashboardController < ApplicationController
def index
@recent_activity = Activity.recent.limit(5)
# Load expensive data only when needed
end
def statistics
# Separate endpoint for deferred content
@statistics = StatisticsService.calculate
render partial: "statistics"
end
end
<!-- Initial render includes critical content -->
<div class="dashboard">
<section class="recent-activity">
<%= render @recent_activity %>
</section>
<!-- Placeholder for deferred content -->
<section data-controller="deferred-content"
data-deferred-content-url-value="<%= dashboard_statistics_path %>">
<div data-deferred-content-target="placeholder">
Loading statistics...
</div>
</section>
</div>
<!-- Enhanced: loads statistics after initial render -->
<!-- Baseline: could include link to statistics page -->
The Optimistic UI Pattern updates interface immediately while processing server request. The UI reflects expected outcome before server confirmation. Server response reconciles state, rolling back optimistic updates if request fails.
# Controller processes request normally
class TodosController < ApplicationController
def complete
@todo = Todo.find(params[:id])
if @todo.update(completed: true)
respond_to do |format|
format.html { redirect_to todos_path }
format.turbo_stream # Confirms completion
end
else
respond_to do |format|
format.html { redirect_to todos_path, alert: "Failed" }
format.turbo_stream {
# Revert optimistic update
render turbo_stream: turbo_stream.replace(
dom_id(@todo),
partial: "todo",
locals: { todo: @todo }
)
}
end
end
end
end
The Feature Flag Pattern conditionally enables enhancements based on user preferences or A/B tests. The application tracks enhancement preferences, rendering appropriate versions.
module ApplicationHelper
def enhanced_feature?(feature_name)
return false unless current_user
current_user.feature_flags.enabled?(feature_name) &&
browser_supports?(feature_name)
end
def browser_supports?(feature_name)
case feature_name
when :turbo_streams
request.headers["Turbo-Frame"].present?
when :webp_images
request.accept.include?("image/webp")
else
true
end
end
end
<% if enhanced_feature?(:infinite_scroll) %>
<%= render "enhanced_product_list" %>
<% else %>
<%= render "paginated_product_list" %>
<%= paginate @products %>
<% end %>
Design Considerations
Progressive enhancement involves architectural decisions affecting application structure, development workflow, and user experience. These considerations guide implementation choices.
Development Sequence determines whether teams build baseline first or retrofit enhancements to existing code. Building baseline-first ensures core functionality receives primary attention and testing. This approach prevents situations where baseline versions appear as afterthoughts with incomplete testing. Starting with HTML, then CSS, then JavaScript naturally enforces layer independence.
Retrofitting enhancements to baseline implementations risks tightly coupling layers. Teams must explicitly design enhancement points during baseline development. This requires identifying where enhancements attach, what data they need, and how they gracefully degrade.
Testing Coverage Distribution allocates testing resources across enhancement levels. Baseline functionality requires comprehensive testing since it represents minimum viable operation. Enhancement features need testing that covers both enhanced and degraded states. Tests verify enhancements activate correctly when capabilities exist and degrade appropriately when absent.
# Test both baseline and enhanced paths
describe "Article creation" do
context "with JavaScript enabled", js: true do
it "submits form via AJAX" do
visit new_article_path
fill_in "Title", with: "Test Article"
click_button "Create"
expect(page).to have_current_path(new_article_path)
expect(page).to have_content("Article created")
# Verifies page didn't reload
end
end
context "without JavaScript" do
it "submits form with standard POST" do
visit new_article_path
fill_in "Title", with: "Test Article"
click_button "Create"
expect(page).to have_current_path(article_path(Article.last))
# Verifies standard redirect happened
end
end
end
Performance Trade-offs balance initial load performance against enhanced experience. Baseline HTML pages load faster than JavaScript-heavy applications but may trigger more server requests. Enhanced versions reduce server load through client-side processing but increase initial payload.
Progressive enhancement enables shipping smaller initial bundles by deferring enhancement code. Critical path includes only baseline rendering. Enhancement scripts load asynchronously or through lazy loading patterns. This improves time-to-interactive metrics while maintaining enhanced capabilities.
Maintenance Overhead increases when supporting multiple experience tiers. Developers implement features across baseline and enhanced versions. Changes require testing both paths. This duplication impacts development velocity but ensures broader compatibility.
Shared component architecture reduces duplication. Components encapsulate baseline and enhanced rendering logic. The application selects appropriate rendering paths at runtime rather than maintaining separate implementations.
User Experience Consistency balances uniform experience across devices against optimized experiences per capability. Strictly uniform experiences forgo enhancement opportunities. Device-specific optimizations risk fragmenting user experience when users switch devices.
Progressive enhancement provides consistency through baseline functionality while allowing device-appropriate enhancements. Users receive functionally equivalent applications across devices with presentation and interaction optimization per environment.
Accessibility Implications improve when progressive enhancement prioritizes semantic HTML and keyboard navigation. Enhancement layers add convenience without replacing accessible baseline patterns. This approach ensures assistive technology users access full functionality.
ARIA attributes enhance but never substitute for proper semantic markup. JavaScript interactions supplement rather than replace keyboard-accessible controls. Enhanced versions maintain focus management and screen reader announcements.
API Design Impact affects endpoint structure when supporting multiple response formats. Single endpoints handling HTML, JSON, and Turbo Stream responses grow complex. Separate API endpoints maintain cleaner separation but require coordinating multiple controllers.
# Monolithic controller handles all formats
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
respond_to do |format|
format.html
format.json { render json: ArticleSerializer.new(@article) }
format.turbo_stream { render partial: "article_detail" }
end
end
end
# Alternative: separate API namespace
module API
class ArticlesController < APIController
def show
article = Article.find(params[:id])
render json: ArticleSerializer.new(article)
end
end
end
Cache Strategy Complexity increases when caching multiple response formats. Fragment caching must account for format variations. CDN caching requires Vary headers indicating content negotiation. This complexity affects cache hit rates and invalidation strategies.
Reference
Enhancement Layers
| Layer | Technology | Baseline Role | Enhancement Role |
|---|---|---|---|
| Content | HTML | Semantic structure, full content | Progressive disclosure, dynamic content |
| Presentation | CSS | Basic layout, readable typography | Advanced layouts, animations, responsive design |
| Behavior | JavaScript | None required | Interactions, validation, dynamic updates |
| Transport | HTTP | Request/response cycle | WebSocket, Server-Sent Events, HTTP/2 Push |
Ruby/Rails Progressive Enhancement Tools
| Tool | Purpose | Baseline Behavior | Enhanced Behavior |
|---|---|---|---|
| Turbo Frames | Page section updates | Full page navigation | In-frame navigation |
| Turbo Streams | DOM updates | Standard redirects | Targeted DOM operations |
| Stimulus | Behavior attachment | Static HTML | Interactive components |
| respond_to | Format negotiation | HTML response | JSON, Turbo Stream responses |
| form_with | Form helpers | Standard POST submission | Remote submission |
| ActionCable | Real-time updates | Polling or manual refresh | WebSocket push |
Implementation Checklist
| Stage | Baseline Requirements | Enhancement Opportunities |
|---|---|---|
| HTML | Semantic elements, accessible forms, proper heading hierarchy | Custom elements, data attributes for enhancement hooks |
| CSS | Readable without images, functional without flexbox/grid | Feature queries, modern layout, animations |
| JavaScript | Zero JavaScript required | Progressive feature detection, modular loading |
| Forms | POST to server, server-side validation | Client validation, AJAX submission, inline errors |
| Navigation | Standard links and redirects | Intercept and AJAX load, history API |
| Data Loading | Server-rendered content | Lazy loading, infinite scroll, prefetching |
Feature Detection Patterns
| Feature | Detection Method | Fallback Strategy |
|---|---|---|
| JavaScript | noscript elements, server detection | Full HTML rendering |
| CSS Grid | Feature queries | Flexbox or float layouts |
| Fetch API | typeof window.fetch | XMLHttpRequest or form submission |
| IntersectionObserver | Check window object | Load all images immediately |
| LocalStorage | Try-catch access | Session-based or server-side storage |
| WebSocket | typeof WebSocket | HTTP polling or Server-Sent Events |
Response Format Matrix
| Request Type | Accept Header | Response Format | Use Case |
|---|---|---|---|
| Browser navigation | text/html | Full HTML page | Baseline navigation |
| AJAX request | application/json | JSON data | Enhanced data loading |
| Turbo request | text/vnd.turbo-stream.html | Turbo Stream | Enhanced page updates |
| API client | application/json | JSON API response | Programmatic access |
| Mobile app | application/json | JSON with metadata | Native app integration |
Validation Strategy Levels
| Level | Technology | Timing | Purpose |
|---|---|---|---|
| HTML5 | required, type, pattern attributes | On submit | Basic client-side prevention |
| JavaScript | Custom validation logic | Real-time during input | User experience improvement |
| Server | Model validations, strong params | On request | Security and data integrity |
| Database | Constraints, indexes | On commit | Final integrity guarantee |
Common Enhancement Points
| UI Element | Baseline Implementation | Enhancement Options |
|---|---|---|
| Dropdown Menu | Details/summary or CSS-only | JavaScript navigation, ARIA management |
| Modal Dialog | Separate page or anchor link | Overlay with backdrop, focus trap |
| Tabs | Anchor links to sections | Show/hide without navigation |
| Sortable Table | Query string sorting | Client-side sort without reload |
| Infinite Scroll | Pagination links | Intersection Observer loading |
| File Upload | Standard file input | Drag-drop, preview, progress |
| Autocomplete | Regular search form | Suggestion dropdown |
| Image Gallery | Grid of linked images | Lightbox, keyboard navigation |