Modern CSS organization (in Rails)
I recently launched Rails Designers. The underlying platform, Forge, will soon be available for sale. I wanted to highlight some modern CSS techniques I used in Forge as CSS has changed massively and added so much good stuff the last years. I wanted to quickly go over some of the techniques used. Make sure to also check out the article on modern CSS features.
Most of what is covered here works in any app using CSS, but the Propshaft bits are Rails-specific. By default new Rails apps add this in your <head>:
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
This results in something like this:
<link rel="stylesheet" href="/assets/animations-098cfc69.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/application-4431eacd.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components-734638f8.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components.partialsfx-b6376766.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/buttons-6da1df21.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/cards-7a8a1bb1.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/channel-ecaacfc7.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/chrome-f0d95aad.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/content-125f6836.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/dialog-e6c35362.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/components/footer-7647b14e.css" data-turbo-track="reload" />
<!-- etc. -->
But I’d like to load the one “application.css” instead:
<%= stylesheet_link_tag :application, "data-turbo-track": "reload" %>
Located at: app/assets/stylesheets/application.css. Results in:
<link rel="stylesheet" href="/assets/application-4431eacd.css" data-turbo-track="reload" />
This means you can write separate Stylesheets for other parts of your app (e.g. public pages and the admin).
<%= stylesheet_link_tag :pages, "data-turbo-track": "reload" %>
CSS Layers
CSS Layers are part of modern CSS and now widely available in browsers. They let you control exactly how styles cascade, making your CSS more predictable. You might recognize this from Tailwind CSS (which now also uses these).
Example from Forge:
@layer reset, variables, defaults, components, utilities;
@import url("./reset.css") layer(reset);
@import url("./variables.css") layer(variables);
@import url("./defaults.css") layer(defaults);
@import url("./components.css") layer(components);
@import url("./components.partialsfx.css") layer(components);
@import url("./utilities.css") layer(utilities);
CSS layers solve the cascade problem by explicitly defining which styles take precedence, regardless of loading order or specificity.
For example, when you define:
@layer reset, components, utilities;
You’re telling the browser that utilities always win over components, which always win over reset styles.
Without layers, if you have:
/* components.css */
.btn { padding: .5rem 1rem; }
/* utilities.css */
.p-0 { padding: 0; }
And use <button class="btn p-0">, the winning style depends on which file loads last or has higher specificity.
With layers, it’s predictable:
@layer components {
.btn { padding: .5rem 1rem; }
}
@layer utilities {
.p-0 { padding: 0; }
}
Now .p-0 always wins because the utilities layer has higher priority than components, even if the components styles are loaded later or have higher specificity. This makes CSS much more maintainable.
Now let’s look at each CSS file from the Forge app:
Reset
This is essentially just modern normalize.
Variables
:root {
color-scheme: light dark;
--color-value: 40;
/* an article detailing the `oklch` usage for easy theming is coming! */
--primary-color: light-dark(oklch(.7 0.18 var(--color-value)), oklch(.65 0.18 var(--color-value)));
--base-color: oklch(0 0 var(--color-value));
/* etc. */
Defaults
Top-level elements/selectors:
body {
tab-size: 4;
line-height: 1.5;
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-font-variant-ligatures: common-ligatures;
font-feature-settings: "liga","clig off";
font-variant-ligatures: common-ligatures;
}
a:any-link {
text-decoration-line: none;
color: inherit;
&:not([class]) {
text-decoration-line: underline;
color: var(--primary-color);
&:hover {
text-decoration-line: none;
}
}
}
// etc.
Components
Nothing more than an index for all components. Each is scoped to its own layer:
@import url("./components/buttons.css") layer(components.buttons);
@import url("./components/chrome.css") layer(components.chrome);
@import url("./components/content.css") layer(components.content);
@import url("./components/dialog.css") layer(components.dialog);
@import url("./components/form.css") layer(components.form);
// etc.
Utilities
Also an index for all utilities. These are small one-off utility classes that can be used in multiple places. You might recognize the approach from Tailwind CSS.
@import url("./utilities/colors.css") layer(utilities.colors);
@import url("./utilities/grid.css") layer(utilities.grid);
@import url("./utilities/spacing.css") layer(utilities.spacing);
@import url("./utilities/visibility.css") layer(utilities.visibility);
Product-minded Rails notes
Once a month: straightforward notes on improving UX in Rails—what to simplify, what to measure, and UI/frontend changes that move real usage.
Use CSS Custom properties (variables)
The previously defined variables can be used to cleanly build reusable components. Much like Bootstrap (and others) is built. Example:
.btn {
display: var(--btn-display, inline flex);
gap: var(--btn-gap, .4em);
align-items: var(--btn-align-items, center);
width: var(--btn-width, auto); height: var(--btn-height, auto);
padding: var(--btn-padding-y, .375rem) var(--btn-padding-x, 1rem);
font-size: var(--btn-font-size, .875rem);
line-height: var(--btn-line-height, 1.5);
font-weight: var(--btn-font-weight, 500);
text-align: var(--btn-text-align, left);
color: var(--btn-color, --base-100, #000);
background: var(--btn-background, none);
border: var(--btn-border-width, 1px) solid var(--btn-border-color, transparent);
border-radius: var(--btn-border-radius, .5rem);
box-shadow: 0 0 0 1px var(--btn-box-shadow-color, transparent);
cursor: var(--btn-cursor, pointer);
transition: all 200ms ease-in-out;
&:hover {
color: var(--btn-hover-color, --base-100, #000);
background: var(--btn-hover-background, none);
box-shadow: 0 0 0 1px var(--btn-hover-box-shadow-color, transparent);
}
}
.btn-primary {
--btn-color: var(--paper-color);
--btn-background:
linear-gradient(var(--primary-color), var(--primary-color)) padding-box,
linear-gradient(oklch(from var(--primary-color) l c h / 5%) 0%, oklch(from var(--primary-color) calc(l - .06) c h) 100%) border-box;
--btn-hover-color: var(--paper-color);
--btn-hover-background:
linear-gradient(var(--primary-color), var(--primary-color)) padding-box,
linear-gradient(oklch(from var(--primary-color) l c h / 5%) 0%, oklch(from var(--primary-color) 1 c h) 100%) border-box;
&:active {
transform: scale(.95);
}
}
Heavily relying on CSS custom properties allows you to tweak button classes in a nested component:
.modifications {
// …
.btn {
--btn-padding-x: 0;
--btn-color: var(--base-50);
--btn-hover-color: var(--base-70);
}
}
CSS nesting is now a standard feature in modern CSS, letting you write cleaner, more organized styles without preprocessors like Sass. The & refers to the parent selector, just like in Sass.
And there you have it. Some modern CSS organization that definitely helped me keep the CSS for Forge in check. I also have another something in the works that definitely helped me write vanilla CSS (there is a hint in this article 🔍😅).
Want to read me more?
-
Two products, one Rails codebase
Build multiple products from a single Rails codebase using variants and custom configuration class. -
10 Modern CSS Features You Want to Use
CSS is evolving quickly and there many really cool new features that can help you craft beautiful web pages. -
Hidden Gems of Tailwind CSS
Unlock the full potential of Tailwind CSS for your Rails-based SaaS apps with our guide on hidden features. From changing checkbox colors to leveraging peer modifiers and custom variants, enhance your web UIs effortlessly.
Over to you…
What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!
Comments are powered by Chirp Form
{{comment}}