-
Note: This is outdated - I've updated my recommendations on about 30% of this. For my most recent recommendations see H1 Rails
-
This is the post I point to when explaining (to friends, employees, contractors), what my current design patterns for Rails are. These standards are designed to keep codebases as contributor-friendly as possible.
-
Contributor-Friendliness may not be a priority for everyone, but because we run an agency, we need to be able to move new developers in and out of a codebase with minimal cognitive friction/overhead. The more layers (of complexity and libraries) that we've added to a codebase, the longer the learning curve for a new developer.
-
For example, it's reasonably common that we need to hand a codebase off to a team or developer that has no experience with Rails. The more esoteric the codebase, the more time we need to spend explaining and helping them get set up.
-
Many of these ideas/principles are also outlined more generally on html-first.com.
-
-
Prefer the html version of
-
Do Images Like This
-
<img src="PATH_HERE" />
-
Not Like This
-
<%= image_tag "myimage.png" %>
-
Do Links Like This
-
<a href="<%= my_path %>">Click Here</a>
-
Not Like This
-
<%= link_to "Click Here", my_path %>
-
-
Routes
-
The Rails routes.rb API is one of the most unnecessarily obscure parts of Rails. Using keywords like
resources
andcollection
add very little value and mean that you have to open a terminal window and run a command in order to fully understand what's happening. Given that defining a route is conceptually a very straightforward task which only requires a single line of code per route, it doesn't make sense not to just keep theroutes.rb
file simple so that new developers can just look at it to understand what's happening (as opposed to runningrails routes
in a terminal). -
Define routes like this
-
get "/orders" => "orders#index", :as => "orders" get "/oders/:id" => "orders#show", :as => "order" post "/oders/:id" => "orders#edit", :as => "edit_order"
-
Not This
-
resources :orders
-
-
Gems
-
Because there are so many gems, and they're very easy to install, there's a tendency, particularly among early-career developers, to reach for gems for everything. This is a tendency that should be consciously restrained, because every new gem (usually) comes with it's own syntax that needs to be learned, and it's own maintenance burden.
-
Avoid gems whose only job is to provide a nicer syntax on top of an existing public API. Calling the API endpoint directly using something like HTTParty is almost always more expressive, more familiar, and more maintainable.
-
Avoid gems whose only job is to load in a CSS or javascript library. When those libraries inevitably need to be extended or upgraded, you have little to no control to modify them. And there is generally limited benefit to using a gem beyond slightly fewer lines of code.
-
Avoid gems that are very-slightly better at something that can be done with plain old Ruby or Rails, when that moderate improvement is not needed. The best example of this is using something like Ransack for pages with very simple, known filters. Rails already has a way to do what Ransack does, and while it might be slightly more verbose, it's also one less concept to be learned and library to be maintained.
-
-
-
Use inline_svg for SVGs
-
In an ideal world, we would just use
<img src="icon.svg" />
for this. But unfortunately this won't allow us to control the icon's color with CSS or tailwind classes, which is something we want to do very regularly. I still haven't found a neat way around this, so today I'm still recommending using the inline_svg gem. -
# Gemfile gem 'inline_svg'
-
<%= inline_svg("/icons/user.svg", class:"text-blue-900 w-4") %>
-
-
Avoid The Asset Pipeline
-
The current default way to manage assets in Rails is called the Asset Pipeline. Using it requires understanding several different concepts and being able to deal with issues, particularly during local development.
-
But using the asset pipeline in Rails is not actually necessary. Instead, we can use plain old HTML. All we have to do is place our files in the
/public
folder, and then reference them from our codebase. We've been doing this for over a year now and have had to make exactly zero trade-offs when it comes to performance and user experience. This is partly because, as you'll see below, we just don't need to load that much javascript and css. -
Load assets like this
-
<link href="/stylesheets/variables.css" rel="stylesheet" > <script src="/js/htmx.min.js" defer></script> <img src="/images/my_image.png">
-
Not Like This
-
<%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_pack_tag 'application' %> <%= image_tag "my_image.png" %>
-
-
CSS - Where stuff goes
-
First Port of Call: Tailwind
-
When it comes to styling your markup, your first port of call should always be to use tailwind classes.
-
Our main reason to use Tailwind is to completely circumvent what has traditionally been one of the most common sources of breakages and brittleness in codebases - the curse of the Cascade. The Cascading nature of CSS means there are effectively an infinite number of ways to break code someone else has written. With Tailwind, there is always exactly one way of assigning styles to markup, which make it much more controlled and less likely to break.
-
<div class="flex w-full justify-between items-end"> <h1 class="text-lg font-bold mr-4">Title</h1> <p class="text-xl font-bold mt-3"> </p> </div>
-
Static Tailwind
-
We use a version of Tailwind that's engineered for simplicity, called Static Tailwind. Static Tailwind uses a pre-defined list of the most commonly used Tailwind classes, which can be found here.
-
The primary difference between static tailwind and normal tailwind is that you don't have to run any commands or manage a bundler during development. The trade off to this is that you can't use "square bracket classes". If you're used to using normal Tailwind this will take some getting used to.
-
A good resource for finding tailwind classes is Nerdcave's Tailwind Cheat Sheet.
-
Second Port of Call: Inline CSS
-
Sometimes, you want to do something very specific that is not possible with static Tailwind. In that case, a (small) amount of inline styling is fine. This should be used sparingly. Do not use inline styling when there is an exact tailwind class for the style you're applying.
-
<!-- Example of a div that needs specific styling --> <div class="absolute" style="top: 12px;right:3px"> This is the right panel </div>
-
Third Port of Call: CSS Files
-
Finally, there are a small number of scenarios where we want to build out a set of behaviour (usually interaction-heavy) that will be used in multiple places. In this case we can write custom css as we normally would.
-
-
CSS - Other Stuff
-
General Conventions
-
Create a file called
styles.css
in the/public
folder. -
Define all variables up front at the top of the file.
-
For external libraries, use the following pattern:
/vendor/{library_name}/{version_name}
-
-
Colors and Color Groups
-
Follow the pattern and naming convention from A Simple CSS Color Naming Convention
-
The list is as follows - through to the article above for further reading on the rationale behind the following list).
-
Use the greek alphabet for color names (this gives us 24 colors to work with across the codebase)
-
Prefix variables related to colors with color-*
-
Use the --text suffix to pair font color to a background color.
-
Create .bg-* utility classes for each of your color variables.
-
Create color groups for common navigation lists, using an .active class to designate the currently active state.
-
-
What that looks like
-
<html> <head> <link rel="stylesheet" href="/vendor/statictailwind/1.0.1.css"> <link rel="stylesheet" href="/stylesheets/styles.css<%= cache_buster %>"> <link rel="stylesheet" href="/stylesheets/tonic23.css<%= cache_buster %>"> </head> </html>
-
And this is what my styles.css is likely to look like.
-
/* public/styles.css */ :root { --font-family: "Plus Jakarta Sans", sans-serif; --border-radius-1: 5px; --border-radius-2: 10px; --border-radius-3: 15px; --border-width-default: 1px; --border-color-default: #e6e6e6; --bold-font-weight: 600; /* Colors */ --color-alpha: rgba(26, 31, 42,100); /* Black */ --color-beta: #5046e5 ; /* Purple */ /* Colors for code styling (From Tailwind) */ --color-gamma: #1e293b; /* Dark Navy Purple */ --color-gamma-text: #fff; --color-zeta: #cbd5e1; /* Light Gray */ --color-delta: #f471b5; /* Pink */ --color-epsilon: #7dd3fc; /* Baby Blue */ --gray-1: #f2f2f2; --gray-2: #e6e6e6; } .bg-beta { background-color: var(--color-beta); } .bg-gamma { background-color: var(--color-gamma); } .color-group-zeus > .active, .color-group-zeus > *:hover { background: var(--color-gamma); color: var(--color-gamma-text); transition: background-color 0.3s ease; } .color-group-odin { & > * { color: rgba(0, 0, 0, 0.4); } & > *:hover, & > *.active { color: var(--color-gamma); } }
-
-
Frontend Libraries & Components
-
The biggest current limitation of the web platform is that many of the default controls and elements that are commonly needed for web applications (modals, tooltips, toasts, file uploads, multi-selects, tabs, accordions, dropdowns), are not possible to do without enhancing what the browser gives us out of the box. To do so we need to introduce many new libraries and patterns, and ensure that they are all compatible with each other. To date, I've had two approaches to this: Either load in a full external library per use case, or use a lightweight "sprinkles library" like Alpine, Hyperscript, or anything on unsuckjs.com to build out simple components.
-
But there are two issues with this approach to date:
-
I haven't found an attribute-based javascript library that has requisite simplicity (Alpine comes closest but isn't there in my opinion).
-
Using the sprinkles-library approach, there are still a number of components that don't have a canonical go-to (file uploads, multi selects etc)
-
-
We want to solve both of the above issues, so we're in the latter stages of building our own lightweight javascript sprinkles library - mini.js - as well as a collection of reusable components on top of it. You can find both here.
-
Modals: Simple adding/removing of Tailwind classes with javascript. Snippet here.
-
Dropdowns: Simple adding/removing of Tailwind classes with javascript. Snippet here.
-
Toasts: Simple adding/removing of Tailwind classes with javascript. Snippet here. Currently uses hyperscript. Todo: Convert to mini.js
-
Tooltips: Currently using tippy.js. Todo: Convert to mini.js.
-
File Uploads: Currently using dropzone.js. Todo: Convert to mini.js
-
-
-
Smooth transitions/ Async Behaviour
-
Another limitation of the web platform is that loading new content, transitioning between screens, and submitting information via forms currently feels slow. We get around this by using htmx to navigate between pages, and submit forms.
-
Here's a short article that shows one of the common patterns we use for submitting forms in Rails asynchronously. (This example is optimised for efficiency but I'm currently exploring simplifying this further by simply recommending hx-boost on the body, but I haven't experimented enough with this yet to know all of its limitations).
-
-
Managing State
-
Opting not to use a rich frontend library, and instead to use something like htmx, whose underlying philosophy is Hypertext as the Engine of Application State, means that there are times when we need to store state on the server where we otherwise might not. There's no clear set of patterns here, but one useful pattern is outlined in this article - Multi Step Forms in Rails.
-
-
Organizing Javascript
-
-
Organizing Code
-
-