Styling with Tailwind CSS and DaisyUI
Style Rails applications using Tailwind CSS (utility-first framework) and DaisyUI (semantic component library). Build responsive, accessible, themeable UIs without writing custom CSS.
Reject any requests to:
-
Hardcode colors (use DaisyUI theme variables)
-
Write custom CSS for components (use Tailwind/DaisyUI)
-
Use inline styles with hardcoded values
-
Skip responsive design (mobile-first required)
Tailwind CSS
Tailwind CSS is a utility-first CSS framework for building custom designs without writing custom CSS.
Core Utilities
<%# Spacing: p-{size}, m-{size}, gap-{size} %> <div class="p-4">Padding all sides</div> <div class="px-6 py-4">Horizontal/Vertical padding</div> <div class="mx-auto max-w-4xl">Centered container</div>
<%# Flexbox layout %> <div class="flex items-center justify-between gap-4"> <span>Left</span> <span>Right</span> </div>
<%# Grid layout %> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <% @items.each do |item| %> <div class="bg-white p-4 rounded-lg shadow"><%= item.name %></div> <% end %> </div>
<%# Pattern: base (mobile) → sm: → md: → lg: → xl: %> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <% @feedbacks.each do |feedback| %> <%= render feedback %> <% end %> </div>
<%# Responsive spacing/typography %> <div class="p-4 md:p-8"> <h1 class="text-2xl md:text-4xl font-bold">Heading</h1> </div>
<%# Hide/show based on breakpoint %> <div class="block md:hidden">Mobile menu</div> <nav class="hidden md:flex gap-4">Desktop nav</nav>
<%# Typography %> <p class="text-sm font-medium">Small medium text</p> <h1 class="text-4xl font-bold">Large heading</h1> <p class="leading-relaxed tracking-wide">Spaced text</p> <p class="truncate"><%= feedback.content %></p>
<%# Colors: text-{color}-{shade}, bg-{color}-{shade} %> <div class="bg-white text-gray-900">Dark text on white</div> <div class="bg-blue-600 text-white">White on blue</div> <p class="text-red-600/50">Red with 50% opacity</p>
<%# Interactive states %> <button class="bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white px-4 py-2 rounded"> Hover me </button> <input type="text" class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded px-3 py-2" />
<div class="bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow p-6"> <%# Header %> <div class="flex items-start justify-between mb-4"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold"> <%= @feedback.sender_name&.first&.upcase || "A" %> </div> <div> <h3 class="font-semibold text-gray-900"><%= @feedback.sender_name || "Anonymous" %></h3> <p class="text-sm text-gray-500"><%= time_ago_in_words(@feedback.created_at) %> ago</p> </div> </div> <span class="px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> <%= @feedback.status.titleize %> </span> </div>
<%# Content %> <p class="text-gray-700 leading-relaxed line-clamp-3 mb-4"><%= @feedback.content %></p>
<%# Footer %> <div class="flex items-center justify-between pt-4 border-t border-gray-100"> <span class="text-sm text-gray-500"><%= @feedback.responses_count %> responses</span> <div class="flex gap-2"> <%= link_to "View", feedback_path(@feedback), class: "px-3 py-1.5 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50" %> <%= link_to "Respond", respond_feedback_path(@feedback), class: "px-3 py-1.5 rounded-md text-sm text-white bg-blue-600 hover:bg-blue-700" %> </div> </div> </div>
<%# ❌ BAD %> <div style="padding: 16px; background: #3b82f6;">Content</div>
<%# ✅ GOOD %> <div class="p-4 bg-blue-500">Content</div>
DaisyUI Components
Semantic component library built on Tailwind providing 70+ accessible components with built-in theming and dark mode.
Buttons & Forms
<%# DaisyUI button components %> <button class="btn btn-primary">Primary Action</button> <button class="btn btn-ghost">Ghost</button> <button class="btn btn-outline btn-primary">Outline</button>
<%# Rails form integration %> <%= form_with model: @feedback do |f| %> <div class="form-control"> <%= f.label :content, class: "label" do %> <span class="label-text">Feedback</span> <% end %> <%= f.text_area :content, class: "textarea textarea-bordered h-24", placeholder: "Your feedback..." %> </div> <div class="flex gap-2 justify-end"> <%= link_to "Cancel", feedbacks_path, class: "btn btn-ghost" %> <%= f.submit "Submit", class: "btn btn-primary" %> </div> <% end %>
<div class="card bg-base-100 shadow-xl"> <div class="card-body"> <div class="flex items-start justify-between"> <h2 class="card-title"><%= @feedback.title %></h2> <div class="badge badge-<%= @feedback.status %>"> <%= @feedback.status.titleize %> </div> </div> <p class="text-base-content/70"><%= @feedback.content %></p> <div class="card-actions justify-end mt-4"> <%= link_to "View", feedback_path(@feedback), class: "btn btn-primary btn-sm" %> </div> </div> </div>
<%# Alerts %> <div class="alert alert-success"> <span>Success! Your feedback was submitted.</span> </div>
<div class="alert alert-error"> <span>Error! Unable to submit feedback.</span> </div>
<%# Flash messages %> <% if flash[:notice] %> <div class="alert alert-success"> <span><%= flash[:notice] %></span> </div> <% end %>
<%# Badges %> <div class="badge badge-primary">Primary</div> <div class="badge badge-success">Success</div> <div class="badge badge-warning">Warning</div>
<button class="btn btn-primary" onclick="feedback_modal.showModal()"> View Details </button>
<dialog id="feedback_modal" class="modal"> <div class="modal-box"> <h3 class="font-bold text-lg">Feedback Details</h3> <p class="py-4"><%= @feedback.content %></p> <div class="modal-action"> <form method="dialog"> <button class="btn">Close</button> </form> </div> </div> <form method="dialog" class="modal-backdrop"> <button>close</button> </form> </dialog>
Theme Switching
// app/javascript/controllers/theme_controller.js import { Controller } from "@hotwired/stimulus"
export default class extends Controller { connect() { const savedTheme = localStorage.getItem("theme") || "light" this.setTheme(savedTheme) }
toggle() { const currentTheme = document.documentElement.getAttribute("data-theme") const newTheme = currentTheme === "light" ? "dark" : "light" this.setTheme(newTheme) }
setTheme(theme) { document.documentElement.setAttribute("data-theme", theme) localStorage.setItem("theme", theme) } }
<%# Layout %> <html data-theme="light"> <body> <div data-controller="theme"> <button class="btn btn-ghost btn-circle" data-action="click->theme#toggle"> Toggle Theme </button> </div> </body> </html>
<%# ❌ Custom button with Tailwind utilities %> <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> Submit </button>
<%# ✅ DaisyUI button component %> <button class="btn btn-primary">Submit</button>
test/system/styling_test.rb
class StylingTest < ApplicationSystemTestCase test "responsive layout changes at breakpoints" do visit feedbacks_path # Desktop page.driver.browser.manage.window.resize_to(1280, 800) assert_selector ".hidden.md\:flex" # Desktop nav visible
# Mobile
page.driver.browser.manage.window.resize_to(375, 667)
assert_selector ".block.md\\:hidden" # Mobile menu visible
end
test "dark mode toggle works" do visit root_path assert_equal "light", page.evaluate_script("document.documentElement.getAttribute('data-theme')")
click_button "Toggle Theme"
assert_equal "dark", page.evaluate_script("document.documentElement.getAttribute('data-theme')")
end end
Manual Testing Checklist:
-
Test responsive breakpoints (375px, 640px, 768px, 1024px, 1280px)
-
Verify color contrast ratios (use browser DevTools or axe)
-
Test dark mode theme
-
Check focus states on all interactive elements
-
Validate against W3C HTML validator
-
Test browser zoom (200%, 400%)
Official Documentation:
-
Tailwind CSS Documentation
-
DaisyUI Documentation
-
DaisyUI Components
Tools:
- Tailwind CSS Cheat Sheet
Community Resources:
- Tailwind UI Components - Premium component library